git.iko.soy/typeable/minio-hs
on master help sign in
Update code formatting and update dependencies (unliftio, protolude) (#152)

* Format code with ormolu * Use latest unliftio-core * Use latest protolude

commit23fecbb469a68de61fa6609d8210a84857ae1503 authorAditya Manthramurthy <donatello@users.noreply.github.com> dateSun 14 Jun 2020, 10:06 (6 years ago) parentce23f73
58 files changed+4991−4175
.github/workflows/haskell.yml+1−1 Setup.hs+1 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+19−21 examples/PutObject.hs+11−14 examples/RemoveBucket.hs+3−8 examples/RemoveIncompleteUpload.hs+7−10 examples/RemoveObject.hs+7−8 examples/SelectObject.hs+24−26 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+3−3 src/Lib/Prelude.hs+37−11 src/Network/Minio.hs+233−216 src/Network/Minio/API.hs+183−156 src/Network/Minio/APICommon.hs+27−23 src/Network/Minio/AdminAPI.hs+470−384 src/Network/Minio/CopyObject.hs+49−33 src/Network/Minio/Data.hs+581−490 src/Network/Minio/Data/ByteString.hs+17−17 src/Network/Minio/Data/Crypto.hs+25−25 src/Network/Minio/Data/Time.hs+11−13 src/Network/Minio/Errors.hs+48−47 src/Network/Minio/JsonParser.hs+23−16 src/Network/Minio/ListOps.hs+65−40 src/Network/Minio/PresignedOperations.hs+201−145 src/Network/Minio/PutObject.hs+84−68 src/Network/Minio/S3API.hs+455−322 src/Network/Minio/SelectAPI.hs+228−233 src/Network/Minio/Sign/V4.hs+318−300 src/Network/Minio/Utils.hs+127−98 src/Network/Minio/XmlGenerator.hs+158−93 src/Network/Minio/XmlParser.hs+106−114 stack.yaml+4−2 stack.yaml.lock+19−5 test/LiveServer.hs+623−456 test/Network/Minio/API/Test.hs+73−61 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+297−276 test/Spec.hs+78−79
.github/workflows/haskell.yml+1−1
@@ -47,7 +47,7 @@ jobs:
4747 - name: Install dependencies
4848 run: |
4949 cabal v2-update
50− cabal v2-build --only-dependencies --enable-tests --enable-benchmarks --verbose=2
50+ cabal v2-build --only-dependencies --enable-tests --enable-benchmarks
5151 - name: Build
5252 run: cabal v2-build --enable-tests --enable-benchmarks all
5353 - name: Run tests
Setup.hs+1
@@ -15,4 +15,5 @@
1515--
1616
1717import Distribution.Simple
18+
1819main = defaultMain
examples/BucketExists.hs+4−7
@@ -16,20 +16,17 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Control.Monad.IO.Class (liftIO)
24−import Prelude
21+import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
23+import Prelude
2524
2625-- | The following example uses minio's play server at
2726-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
3532 let bucket = "missingbucket"
@@ -39,5 +36,5 @@ main = do
3936 liftIO $ putStrLn $ "Does " ++ show bucket ++ " exist? - " ++ show foundBucket
4037
4138 case res1 of
42− Left e -> putStrLn $ "bucketExists failed." ++ show e
39+ Left e -> putStrLn $ "bucketExists failed." ++ show e
4340 Right () -> return ()
examples/CopyObject.hs+12−14
@@ -16,42 +16,40 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import UnliftIO.Exception (catch, throwIO)
21+import Network.Minio
22+import UnliftIO.Exception (catch, throwIO)
2423
2524-- | The following example uses minio's play server at
2625-- https://play.min.io. The endpoint and associated
2726-- credentials are provided via the libary constant,
2827--
2928-- > minioPlayCI :: ConnectInfo
30−--
31−
3229main :: IO ()
3330main = do
34− let
35− bucket = "test"
31+ let bucket = "test"
3632 object = "obj"
3733 objectCopy = "obj-copy"
3834 localFile = "/etc/lsb-release"
3935
4036 res1 <- runMinio minioPlayCI $ do
4137 -- 1. Make a bucket; Catch BucketAlreadyOwnedByYou exception.
42− catch (makeBucket bucket Nothing) (
43− \e -> case e of
44− BucketAlreadyOwnedByYou -> return ()
45− _ -> throwIO e
38+ catch
39+ (makeBucket bucket Nothing)
40+ ( \e -> case e of
41+ BucketAlreadyOwnedByYou -> return ()
42+ _ -> throwIO e
4643 )
4744
4845 -- 2. Upload a file to bucket/object.
4946 fPutObject bucket object localFile defaultPutObjectOptions
5047
5148 -- 3. Copy bucket/object to bucket/objectCopy.
52− copyObject defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
53− defaultSourceInfo { srcBucket = bucket , srcObject = object }
49+ copyObject
50+ defaultDestinationInfo {dstBucket = bucket, dstObject = objectCopy}
51+ defaultSourceInfo {srcBucket = bucket, srcObject = object}
5452
5553 case res1 of
56− Left e -> putStrLn $ "copyObject failed." ++ show e
54+ Left e -> putStrLn $ "copyObject failed." ++ show e
5755 Right () -> putStrLn "copyObject succeeded."
examples/FileUploader.hs+24−24
@@ -16,40 +16,40 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
20−
21−{-# LANGUAGE OverloadedStrings #-}
19+{-# LANGUAGE OverloadedStrings #-}
2220{-# LANGUAGE ScopedTypeVariables #-}
23−import Network.Minio
2421
25−import Data.Monoid ((<>))
26−import Data.Text (pack)
27−import Options.Applicative
28−import System.FilePath.Posix
29−import UnliftIO (throwIO, try)
30−
31−import Prelude
22+import Data.Monoid ((<>))
23+import Data.Text (pack)
24+import Network.Minio
25+import Options.Applicative
26+import System.FilePath.Posix
27+import UnliftIO (throwIO, try)
28+import Prelude
3229
3330-- | The following example uses minio's play server at
3431-- https://play.min.io. The endpoint and associated
3532-- credentials are provided via the libary constant,
3633--
3734-- > minioPlayCI :: ConnectInfo
38−--
3935
4036-- optparse-applicative package based command-line parsing.
4137fileNameArgs :: Parser FilePath
42−fileNameArgs = strArgument
43− (metavar "FILENAME"
44− <> help "Name of file to upload to AWS S3 or a MinIO server")
38+fileNameArgs =
39+ strArgument
40+ ( metavar "FILENAME"
41+ <> help "Name of file to upload to AWS S3 or a MinIO server"
42+ )
4543
4644cmdParser :: ParserInfo FilePath
47−cmdParser = info
48− (helper <*> fileNameArgs)
49− (fullDesc
50− <> progDesc "FileUploader"
51− <> header
52− "FileUploader - a simple file-uploader program using minio-hs")
45+cmdParser =
46+ info
47+ (helper <*> fileNameArgs)
48+ ( fullDesc
49+ <> progDesc "FileUploader"
50+ <> header
51+ "FileUploader - a simple file-uploader program using minio-hs"
52+ )
5353
5454main :: IO ()
5555main = do
@@ -64,12 +64,12 @@ main = do
6464 bErr <- try $ makeBucket bucket Nothing
6565 case bErr of
6666 Left BucketAlreadyOwnedByYou -> return ()
67− Left e -> throwIO e
68− Right _ -> return ()
67+ Left e -> throwIO e
68+ Right _ -> return ()
6969
7070 -- Upload filepath to bucket; object is derived from filepath.
7171 fPutObject bucket object filepath defaultPutObjectOptions
7272
7373 case res of
74− Left e -> putStrLn $ "file upload failed due to " ++ (show e)
74+ Left e -> putStrLn $ "file upload failed due to " ++ (show e)
7575 Right () -> putStrLn "file upload succeeded."
examples/GetConfig.hs+5−5
@@ -16,15 +16,15 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+import Prelude
2524
2625main :: IO ()
2726main = do
28− res <- runMinio minioPlayCI $
27+ res <-
28+ runMinio minioPlayCI $
2929 getConfig
3030 print res
examples/GetObject.hs+5−10
@@ -16,31 +16,26 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import qualified Data.Conduit as C
21+import qualified Data.Conduit as C
2422import qualified Data.Conduit.Binary as CB
25−
26−import Prelude
23+import Network.Minio
24+import Prelude
2725
2826-- | The following example uses minio's play server at
2927-- https://play.min.io. The endpoint and associated
3028-- credentials are provided via the libary constant,
3129--
3230-- > minioPlayCI :: ConnectInfo
33−--
34−
3531main :: IO ()
3632main = do
37− let
38− bucket = "my-bucket"
33+ let bucket = "my-bucket"
3934 object = "my-object"
4035 res <- runMinio minioPlayCI $ do
4136 src <- getObject bucket object defaultGetObjectOptions
4237 C.connect (gorObjectStream src) $ CB.sinkFileCautious "/tmp/my-object"
4338
4439 case res of
45− Left e -> putStrLn $ "getObject failed." ++ (show e)
40+ Left e -> putStrLn $ "getObject failed." ++ (show e)
4641 Right _ -> putStrLn "getObject succeeded."
examples/HeadObject.hs+8−11
@@ -16,28 +16,25 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.S3API
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.S3API
23+import Prelude
2524
2625-- | The following example uses minio's play server at
2726-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
35− let
36− bucket = "test"
32+ let bucket = "test"
3733 object = "passwd"
38− res <- runMinio minioPlayCI $
39− headObject bucket object []
34+ res <-
35+ runMinio minioPlayCI $
36+ headObject bucket object []
4037
4138 case res of
42− Left e -> putStrLn $ "headObject failed." ++ show e
39+ Left e -> putStrLn $ "headObject failed." ++ show e
4340 Right objInfo -> putStrLn $ "headObject succeeded." ++ show objInfo
examples/Heal.hs+11−7
@@ -16,19 +16,23 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+import Prelude
2524
2625main :: IO ()
2726main = do
2827 res <- runMinio minioPlayCI $
2928 do
30− hsr <- startHeal Nothing Nothing HealOpts { hoRecursive = True
31− , hoDryRun = False
32− }
29+ hsr <-
30+ startHeal
31+ Nothing
32+ Nothing
33+ HealOpts
34+ { hoRecursive = True,
35+ hoDryRun = False
36+ }
3337 getHealStatus Nothing Nothing (hsrClientToken hsr)
3438 print res
examples/ListBuckets.hs+3−5
@@ -16,19 +16,17 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Control.Monad.IO.Class (liftIO)
24−import Prelude
21+import Control.Monad.IO.Class (liftIO)
22+import Network.Minio
23+import Prelude
2524
2625-- | The following example uses minio's play server at
2726-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
3230
3331-- This example list buckets that belongs to the user and returns
3432-- region of the first bucket returned.
examples/ListIncompleteUploads.hs+17−19
@@ -16,38 +16,36 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Conduit
24−import Prelude
21+import Conduit
22+import Network.Minio
23+import Prelude
2524
2625-- | The following example uses minio's play server at
2726-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
35− let
36− bucket = "test"
32+ let bucket = "test"
3733
3834 -- Performs a recursive listing of incomplete uploads under bucket "test"
3935 -- on a local minio server.
40− res <- runMinio minioPlayCI $
41− runConduit $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
36+ res <-
37+ runMinio minioPlayCI
38+ $ runConduit
39+ $ listIncompleteUploads bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
4240 print res
4341
44− {-
45− Following is the output of the above program on a local MinIO server.
42+{-
43+ Following is the output of the above program on a local MinIO server.
4644
47− Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
48− , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
49− , uiInitTime = 2017-03-01 10:16:25.698 UTC
50− , uiSize = 17731794
51− }
52− ]
53− -}
45+ Right [UploadInfo { uiKey = "go1.6.2.linux-amd64.tar.gz"
46+ , uiUploadId = "063eb592-bdd7-4a0c-be48-34fb3ceb63e2"
47+ , uiInitTime = 2017-03-01 10:16:25.698 UTC
48+ , uiSize = 17731794
49+ }
50+ ]
51+-}
examples/ListObjects.hs+13−15
@@ -16,33 +16,31 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−
23−import Conduit
24−import Prelude
2520
21+import Conduit
22+import Network.Minio
23+import Prelude
2624
2725-- | The following example uses minio's play server at
2826-- https://play.min.io. The endpoint and associated
2927-- credentials are provided via the libary constant,
3028--
3129-- > minioPlayCI :: ConnectInfo
32−--
33−
3430main :: IO ()
3531main = do
36− let
37− bucket = "test"
32+ let bucket = "test"
3833
3934 -- Performs a recursive listing of all objects under bucket "test"
4035 -- on play.min.io.
41− res <- runMinio minioPlayCI $
42− runConduit $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
36+ res <-
37+ runMinio minioPlayCI
38+ $ runConduit
39+ $ listObjects bucket Nothing True .| mapM_C (\v -> (liftIO $ print v))
4340 print res
44− {-
45− Following is the output of the above program on a local MinIO server.
4641
47− 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}]
48− -}
42+{-
43+ Following is the output of the above program on a local MinIO server.
44+
45+ 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}]
46+-}
examples/MakeBucket.hs+6−9
@@ -16,24 +16,21 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
20−
2119{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
2320
24−import Prelude
21+import Network.Minio
22+import Prelude
2523
2624-- | The following example uses minio's play server at
2725-- https://play.min.io. The endpoint and associated
2826-- credentials are provided via the libary constant,
2927--
3028-- > minioPlayCI :: ConnectInfo
31−--
32−
3329main :: IO ()
3430main = do
3531 let bucket = "my-bucket"
36− res <- runMinio minioPlayCI $
37− -- N B the region provided for makeBucket is optional.
38− makeBucket bucket (Just "us-east-1")
32+ res <-
33+ runMinio minioPlayCI $
34+ -- N B the region provided for makeBucket is optional.
35+ makeBucket bucket (Just "us-east-1")
3936 print res
examples/PresignedGetObject.hs+30−27
@@ -16,34 +16,32 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Control.Monad.IO.Class (liftIO)
24−import qualified Data.ByteString.Char8 as B
25−import Data.CaseInsensitive (original)
21+import Control.Monad.IO.Class (liftIO)
22+import qualified Data.ByteString.Char8 as B
23+import Data.CaseInsensitive (original)
2624import qualified Data.Conduit.Combinators as CC
27−import qualified Data.Text.Encoding as E
25+import qualified Data.Text.Encoding as E
26+import Network.Minio
2827
2928-- | The following example uses minio's play server at
3029-- https://play.min.io. The endpoint and associated
3130-- credentials are provided via the libary constant,
3231--
3332-- > minioPlayCI :: ConnectInfo
34−--
35−
3633main :: IO ()
3734main = do
38− let
39− bucket = "my-bucket"
40− object = "my-object"
41− kb15 = 15*1024
42−
43− -- Set query parameter to modify content disposition response
44− -- header
45− queryParam = [("response-content-disposition",
46− Just "attachment; filename=\"your-filename.txt\"")]
35+ let bucket = "my-bucket"
36+ object = "my-object"
37+ kb15 = 15 * 1024
38+ -- Set query parameter to modify content disposition response
39+ -- header
40+ queryParam =
41+ [ ( "response-content-disposition",
42+ Just "attachment; filename=\"your-filename.txt\""
43+ )
44+ ]
4745
4846 res <- runMinio minioPlayCI $ do
4947 liftIO $ B.putStrLn "Upload a file that we will fetch with a presigned URL..."
@@ -61,23 +59,28 @@ main = do
6159 -- Generate a URL with 7 days expiry time - note that the headers
6260 -- used above must be added to the request with the signed URL
6361 -- generated.
64− url <- presignedGetObjectUrl "my-bucket" "my-object" (7*24*3600)
65− queryParam headers
62+ url <-
63+ presignedGetObjectUrl
64+ "my-bucket"
65+ "my-object"
66+ (7 * 24 * 3600)
67+ queryParam
68+ headers
6669
6770 return (headers, etag, url)
6871
6972 case res of
7073 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
7174 Right (headers, _, url) -> do
72−
7375 -- We generate a curl command to demonstrate usage of the signed
7476 -- URL.
75− let
76− hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
77− curlCmd = B.intercalate " " $
78− ["curl --fail"] ++ map hdrOpt headers ++
79− ["-o /tmp/myfile", B.concat ["'", url, "'"]]
77+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
78+ curlCmd =
79+ B.intercalate " " $
80+ ["curl --fail"] ++ map hdrOpt headers
81+ ++ ["-o /tmp/myfile", B.concat ["'", url, "'"]]
8082
81− putStrLn $ "The following curl command would use the presigned " ++
82− "URL to fetch the object and write it to \"/tmp/myfile\":"
83+ putStrLn $
84+ "The following curl command would use the presigned "
85+ ++ "URL to fetch the object and write it to \"/tmp/myfile\":"
8386 B.putStrLn curlCmd
examples/PresignedPostPolicy.hs+35−34
@@ -16,47 +16,43 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import qualified Data.ByteString as B
21+import qualified Data.ByteString as B
2422import qualified Data.ByteString.Char8 as Char8
25−import qualified Data.HashMap.Strict as H
26−import qualified Data.Text.Encoding as Enc
27−import qualified Data.Time as Time
23+import qualified Data.HashMap.Strict as H
24+import qualified Data.Text.Encoding as Enc
25+import qualified Data.Time as Time
26+import Network.Minio
2827
2928-- | The following example uses minio's play server at
3029-- https://play.min.io. The endpoint and associated
3130-- credentials are provided via the libary constant,
3231--
3332-- > minioPlayCI :: ConnectInfo
34−--
35−
3633main :: IO ()
3734main = do
3835 now <- Time.getCurrentTime
39− let
40− bucket = "my-bucket"
41− object = "photos/my-object"
42−
43− -- set an expiration time of 10 days
44− expireTime = Time.addUTCTime (3600 * 24 * 10) now
45−
46− -- create a policy with expiration time and conditions - since the
47− -- conditions are validated, newPostPolicy returns an Either value
48− policyE = newPostPolicy expireTime
49− [ -- set the object name condition
50− ppCondKey object
51− -- set the bucket name condition
52− , ppCondBucket bucket
53− -- set the size range of object as 1B to 10MiB
54− , ppCondContentLengthRange 1 (10*1024*1024)
55− -- set content type as jpg image
56− , ppCondContentType "image/jpeg"
57− -- on success set the server response code to 200
58− , ppCondSuccessActionStatus 200
59− ]
36+ let bucket = "my-bucket"
37+ object = "photos/my-object"
38+ -- set an expiration time of 10 days
39+ expireTime = Time.addUTCTime (3600 * 24 * 10) now
40+ -- create a policy with expiration time and conditions - since the
41+ -- conditions are validated, newPostPolicy returns an Either value
42+ policyE =
43+ newPostPolicy
44+ expireTime
45+ [ -- set the object name condition
46+ ppCondKey object,
47+ -- set the bucket name condition
48+ ppCondBucket bucket,
49+ -- set the size range of object as 1B to 10MiB
50+ ppCondContentLengthRange 1 (10 * 1024 * 1024),
51+ -- set content type as jpg image
52+ ppCondContentType "image/jpeg",
53+ -- on success set the server response code to 200
54+ ppCondSuccessActionStatus 200
55+ ]
6056
6157 case policyE of
6258 Left err -> putStrLn $ show err
@@ -66,11 +62,16 @@ main = do
6662
6763 -- a curl command is output to demonstrate using the generated
6864 -- URL and form-data
69− let
70− formFn (k, v) = B.concat ["-F ", Enc.encodeUtf8 k, "=",
71− "'", v, "'"]
72− formOptions = B.intercalate " " $ map formFn $ H.toList formData
73−
65+ let formFn (k, v) =
66+ B.concat
67+ [ "-F ",
68+ Enc.encodeUtf8 k,
69+ "=",
70+ "'",
71+ v,
72+ "'"
73+ ]
74+ formOptions = B.intercalate " " $ map formFn $ H.toList formData
7475
7576 return $ B.intercalate " " $
7677 ["curl", formOptions, "-F file=@/tmp/photo.jpg", url]
examples/PresignedPutObject.hs+19−21
@@ -16,44 +16,42 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
2321import qualified Data.ByteString.Char8 as B
24−import Data.CaseInsensitive (original)
22+import Data.CaseInsensitive (original)
23+import Network.Minio
2524
2625-- | The following example uses minio's play server at
2726-- https://play.min.io. The endpoint and associated
2827-- credentials are provided via the libary constant,
2928--
3029-- > minioPlayCI :: ConnectInfo
31−--
32−
3330main :: IO ()
3431main = do
35− let
36− -- Use headers to set user-metadata - note that this header will
37− -- need to be set when the URL is used to make an upload.
38− headers = [("x-amz-meta-url-creator",
39− "minio-hs-presigned-put-example")]
32+ let -- Use headers to set user-metadata - note that this header will
33+ -- need to be set when the URL is used to make an upload.
34+ headers =
35+ [ ( "x-amz-meta-url-creator",
36+ "minio-hs-presigned-put-example"
37+ )
38+ ]
4039 res <- runMinio minioPlayCI $ do
41−
4240 -- generate a URL with 7 days expiry time
43− presignedPutObjectUrl "my-bucket" "my-object" (7*24*3600) headers
41+ presignedPutObjectUrl "my-bucket" "my-object" (7 * 24 * 3600) headers
4442
4543 case res of
4644 Left e -> putStrLn $ "presignedPutObject URL failed." ++ show e
4745 Right url -> do
48−
4946 -- We generate a curl command to demonstrate usage of the signed
5047 -- URL.
51− let
52− hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
53− curlCmd = B.intercalate " " $
54− ["curl "] ++ map hdrOpt headers ++
55− ["-T /tmp/myfile", B.concat ["'", url, "'"]]
56−
57− putStrLn $ "The following curl command would use the presigned " ++
58− "URL to upload the file at \"/tmp/myfile\":"
48+ let hdrOpt (k, v) = B.concat ["-H '", original k, ": ", v, "'"]
49+ curlCmd =
50+ B.intercalate " " $
51+ ["curl "] ++ map hdrOpt headers
52+ ++ ["-T /tmp/myfile", B.concat ["'", url, "'"]]
53+
54+ putStrLn $
55+ "The following curl command would use the presigned "
56+ ++ "URL to upload the file at \"/tmp/myfile\":"
5957 B.putStrLn curlCmd
examples/PutObject.hs+11−14
@@ -16,39 +16,36 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
2321import qualified Data.Conduit.Combinators as CC
24−
25−import Prelude
22+import Network.Minio
23+import Prelude
2624
2725-- | The following example uses minio's play server at
2826-- https://play.min.io. The endpoint and associated
2927-- credentials are provided via the libary constant,
3028--
3129-- > minioPlayCI :: ConnectInfo
32−--
33−
3430main :: IO ()
3531main = do
36− let
37− bucket = "test"
32+ let bucket = "test"
3833 object = "obj"
3934 localFile = "/etc/lsb-release"
4035 kb15 = 15 * 1024
4136
4237 -- Eg 1. Upload a stream of repeating "a" using putObject with default options.
43− res1 <- runMinio minioPlayCI $
44− putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
38+ res1 <-
39+ runMinio minioPlayCI $
40+ putObject bucket object (CC.repeat "a") (Just kb15) defaultPutObjectOptions
4541 case res1 of
46− Left e -> putStrLn $ "putObject failed." ++ show e
42+ Left e -> putStrLn $ "putObject failed." ++ show e
4743 Right () -> putStrLn "putObject succeeded."
4844
4945 -- Eg 2. Upload a file using fPutObject with default options.
50− res2 <- runMinio minioPlayCI $
51− fPutObject bucket object localFile defaultPutObjectOptions
46+ res2 <-
47+ runMinio minioPlayCI $
48+ fPutObject bucket object localFile defaultPutObjectOptions
5249 case res2 of
53− Left e -> putStrLn $ "fPutObject failed." ++ show e
50+ Left e -> putStrLn $ "fPutObject failed." ++ show e
5451 Right () -> putStrLn "fPutObject succeeded."
examples/RemoveBucket.hs+3−8
@@ -16,23 +16,18 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
20−
2119{-# LANGUAGE OverloadedStrings #-}
22−import Network.Minio
2320
24−import Prelude
21+import Network.Minio
22+import Prelude
2523
2624-- | The following example uses minio's play server at
2725-- https://play.min.io. The endpoint and associated
2826-- credentials are provided via the libary constant,
2927--
3028-- > minioPlayCI :: ConnectInfo
31−--
32−
3329main :: IO ()
3430main = do
35− let
36− bucket = "my-bucket"
31+ let bucket = "my-bucket"
3732 res <- runMinio minioPlayCI $ removeBucket bucket
3833 print res
examples/RemoveIncompleteUpload.hs+7−10
@@ -16,27 +16,24 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
2220
23−import Prelude
21+import Network.Minio
22+import Prelude
2423
2524-- | The following example uses minio's play server at
2625-- https://play.min.io. The endpoint and associated
2726-- credentials are provided via the libary constant,
2827--
2928-- > minioPlayCI :: ConnectInfo
30−--
31−
3229main :: IO ()
3330main = do
34− let
35− bucket = "mybucket"
36− object = "myobject"
31+ let bucket = "mybucket"
32+ object = "myobject"
3733
38− res <- runMinio minioPlayCI $
39− removeIncompleteUpload bucket object
34+ res <-
35+ runMinio minioPlayCI $
36+ removeIncompleteUpload bucket object
4037
4138 case res of
4239 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/RemoveObject.hs+7−8
@@ -16,20 +16,19 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
2120
22−import Network.Minio
23−import Prelude
21+import Network.Minio
22+import Prelude
2423
2524main :: IO ()
2625main = do
27− let
28− bucket = "mybucket"
29− object = "myobject"
26+ let bucket = "mybucket"
27+ object = "myobject"
3028
31− res <- runMinio minioPlayCI $
32− removeObject bucket object
29+ res <-
30+ runMinio minioPlayCI $
31+ removeObject bucket object
3332
3433 case res of
3534 Left _ -> putStrLn $ "Failed to remove " ++ show bucket ++ "/" ++ show object
examples/SelectObject.hs+24−26
@@ -16,34 +16,32 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−
23−import qualified Conduit as C
24−import Control.Monad (when)
2520
26−import Prelude
21+import qualified Conduit as C
22+import Control.Monad (when)
23+import Network.Minio
24+import Prelude
2725
2826main :: IO ()
2927main = do
30− let bucket = "selectbucket"
31− object = "1.csv"
32− content = "Name,Place,Temperature\n"
33− <> "James,San Jose,76\n"
34− <> "Alicia,San Leandro,88\n"
35− <> "Mark,San Carlos,90\n"
36−
37− res <- runMinio minioPlayCI $ do
38−
39− exists <- bucketExists bucket
40− when (not exists) $
41− makeBucket bucket Nothing
42−
43− C.liftIO $ putStrLn "Uploading csv object"
44− putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
45−
46− let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
47− res <- selectObjectContent bucket object sr
48− C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
49− print res
28+ let bucket = "selectbucket"
29+ object = "1.csv"
30+ content =
31+ "Name,Place,Temperature\n"
32+ <> "James,San Jose,76\n"
33+ <> "Alicia,San Leandro,88\n"
34+ <> "Mark,San Carlos,90\n"
35+
36+ res <- runMinio minioPlayCI $ do
37+ exists <- bucketExists bucket
38+ when (not exists) $
39+ makeBucket bucket Nothing
40+
41+ C.liftIO $ putStrLn "Uploading csv object"
42+ putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
43+
44+ let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
45+ res <- selectObjectContent bucket object sr
46+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
47+ print res
examples/ServerInfo.hs+6−6
@@ -16,15 +16,15 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+import Prelude
2524
2625main :: IO ()
2726main = do
28− res <- runMinio minioPlayCI $
29− getServerInfo
27+ res <-
28+ runMinio minioPlayCI $
29+ getServerInfo
3030 print res
examples/ServiceSendRestart.hs+6−6
@@ -16,15 +16,15 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+import Prelude
2524
2625main :: IO ()
2726main = do
28− res <- runMinio minioPlayCI $
29− serviceSendAction ServiceActionRestart
27+ res <-
28+ runMinio minioPlayCI $
29+ serviceSendAction ServiceActionRestart
3030 print res
examples/ServiceSendStop.hs+6−6
@@ -16,15 +16,15 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+import Prelude
2524
2625main :: IO ()
2726main = do
28− res <- runMinio minioPlayCI $
29− serviceSendAction ServiceActionStop
27+ res <-
28+ runMinio minioPlayCI $
29+ serviceSendAction ServiceActionStop
3030 print res
examples/ServiceStatus.hs+6−6
@@ -16,15 +16,15 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+import Prelude
2524
2625main :: IO ()
2726main = do
28− res <- runMinio minioPlayCI $
29− serviceStatus
27+ res <-
28+ runMinio minioPlayCI $
29+ serviceStatus
3030 print res
examples/SetConfig.hs+3−4
@@ -16,12 +16,11 @@
1616-- See the License for the specific language governing permissions and
1717-- limitations under the License.
1818--
19−
2019{-# LANGUAGE OverloadedStrings #-}
21−import Network.Minio
22−import Network.Minio.AdminAPI
2320
24−import Prelude
21+import Network.Minio
22+import Network.Minio.AdminAPI
23+import Prelude
2524
2625main :: IO ()
2726main = do
minio-hs.cabal+3−3
@@ -56,7 +56,7 @@ common base-settings
5656 , Network.Minio.XmlParser
5757 , Network.Minio.JsonParser
5858 build-depends: base >= 4.7 && < 5
59− , protolude >= 0.2 && < 0.3
59+ , protolude >= 0.3 && < 0.4
6060 , aeson >= 1.2
6161 , base64-bytestring >= 1.0
6262 , binary >= 0.8.5.0
@@ -83,8 +83,8 @@ common base-settings
8383 , text >= 1.2
8484 , time >= 1.8
8585 , transformers >= 0.5
86− , unliftio >= 0.2
87− , unliftio-core >= 0.1 && < 0.2
86+ , unliftio >= 0.2 && < 0.3
87+ , unliftio-core >= 0.2 && < 0.3
8888 , unordered-containers >= 0.2
8989 , xml-conduit >= 1.8
9090
src/Lib/Prelude.hs+37−11
@@ -15,19 +15,45 @@
1515--
1616
1717module Lib.Prelude
18− ( module Exports
19− , both
20− ) where
18+ ( module Exports,
19+ both,
20+ showBS,
21+ toStrictBS,
22+ fromStrictBS,
23+ )
24+where
2125
22−import Protolude as Exports hiding (catch, catches,
23− throwIO, try)
24−
25−import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
26−import Data.Time as Exports (UTCTime (..),
27− diffUTCTime)
28−import UnliftIO as Exports (catch, catches, throwIO,
29− try)
26+import Control.Monad.Trans.Maybe as Exports (MaybeT (..), runMaybeT)
27+import qualified Data.ByteString.Lazy as LB
28+import Data.Time as Exports
29+ ( UTCTime (..),
30+ diffUTCTime,
31+ )
32+import Protolude as Exports hiding
33+ ( Handler,
34+ catch,
35+ catches,
36+ throwIO,
37+ try,
38+ yield,
39+ )
40+import UnliftIO as Exports
41+ ( Handler,
42+ catch,
43+ catches,
44+ throwIO,
45+ try,
46+ )
3047
3148-- | Apply a function on both elements of a pair
3249both :: (a -> b) -> (a, a) -> (b, b)
3350both f (a, b) = (f a, f b)
51+
52+showBS :: Show a => a -> ByteString
53+showBS a = toUtf8 (show a :: Text)
54+
55+toStrictBS :: LByteString -> ByteString
56+toStrictBS = LB.toStrict
57+
58+fromStrictBS :: ByteString -> LByteString
59+fromStrictBS = LB.fromStrict
src/Network/Minio.hs+233−216
@@ -22,218 +22,217 @@
2222--
2323-- Types and functions to conveniently access S3 compatible object
2424-- storage servers like MinIO.
25−
2625module Network.Minio
27− (
28− -- * Credentials
29− Credentials (..)
30−
31− -- ** Credential providers
32− -- | Run actions that retrieve 'Credentials' from the environment or
33− -- files or other custom sources.
34− , Provider
35− , fromAWSConfigFile
36− , fromAWSEnv
37− , fromMinioEnv
38− , findFirst
39−
40− -- * Connecting to object storage
41− , ConnectInfo
42− , setRegion
43− , setCreds
44− , setCredsFrom
45− , isConnectInfoSecure
46− , disableTLSCertValidation
47− , MinioConn
48− , mkMinioConn
49−
50− -- ** Connection helpers
51− -- | These are helpers to construct 'ConnectInfo' values for common
52− -- cases.
53− , minioPlayCI
54− , awsCI
55− , gcsCI
56−
57− -- * Minio Monad
58− ----------------
59− -- | The Minio Monad provides connection-reuse, bucket-location
60− -- caching, resource management and simpler error handling
61− -- functionality. All actions on object storage are performed within
62− -- this Monad.
63− , Minio
64− , runMinioWith
65− , runMinio
66− , runMinioResWith
67− , runMinioRes
68−
69− -- * Bucket Operations
70−
71− -- ** Creation, removal and querying
72− , Bucket
73− , makeBucket
74− , removeBucket
75− , bucketExists
76− , Region
77− , getLocation
78−
79− -- ** Listing buckets
80− , BucketInfo(..)
81− , listBuckets
82−
83− -- ** Listing objects
84− , listObjects
85− , listObjectsV1
86− , ListItem(..)
87−
88− , ObjectInfo
89− , oiObject
90− , oiModTime
91− , oiETag
92− , oiSize
93− , oiUserMetadata
94− , oiMetadata
95−
96− -- ** Listing incomplete uploads
97− , listIncompleteUploads
98− , UploadId
99− , UploadInfo(..)
100− , listIncompleteParts
101− , ObjectPartInfo(..)
102−
103− -- ** Bucket Notifications
104− , getBucketNotification
105− , putBucketNotification
106− , removeAllBucketNotification
107− , Notification(..)
108− , defaultNotification
109− , NotificationConfig(..)
110− , Arn
111− , Event(..)
112− , Filter(..)
113− , defaultFilter
114− , FilterKey(..)
115− , defaultFilterKey
116− , FilterRules(..)
117− , defaultFilterRules
118− , FilterRule(..)
119−
120− -- * Object Operations
121− , Object
122−
123− -- ** File-based operations
124− , fGetObject
125− , fPutObject
126−
127− -- ** Conduit-based streaming operations
128− , putObject
129− , PutObjectOptions
130− , defaultPutObjectOptions
131− , pooContentType
132− , pooContentEncoding
133− , pooContentDisposition
134− , pooContentLanguage
135− , pooCacheControl
136− , pooStorageClass
137− , pooUserMetadata
138− , pooNumThreads
139− , pooSSE
140−
141− , getObject
142− , GetObjectOptions
143− , defaultGetObjectOptions
144− , gooRange
145− , gooIfMatch
146− , gooIfNoneMatch
147− , gooIfModifiedSince
148− , gooIfUnmodifiedSince
149− , gooSSECKey
150− , GetObjectResponse
151− , gorObjectInfo
152− , gorObjectStream
153−
154− -- ** Server-side object copying
155− , copyObject
156− , SourceInfo
157− , defaultSourceInfo
158− , srcBucket
159− , srcObject
160− , srcRange
161− , srcIfMatch
162− , srcIfNoneMatch
163− , srcIfModifiedSince
164− , srcIfUnmodifiedSince
165− , DestinationInfo
166− , defaultDestinationInfo
167− , dstBucket
168− , dstObject
169−
170− -- ** Querying object info
171− , statObject
172−
173− -- ** Object removal operations
174− , removeObject
175− , removeIncompleteUpload
176−
177− -- ** Select Object Content with SQL
178− , module Network.Minio.SelectAPI
179−
180− -- * Server-Side Encryption Helpers
181− , mkSSECKey
182− , SSECKey
183− , SSE(..)
184−
185− -- * Presigned Operations
186− , presignedPutObjectUrl
187− , presignedGetObjectUrl
188− , presignedHeadObjectUrl
189− , UrlExpiry
190−
191− -- ** POST (browser) upload helpers
192− -- | Please see
193− -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
194− -- for detailed information.
195− , newPostPolicy
196− , presignedPostPolicy
197− , showPostPolicy
198− , PostPolicy
199− , PostPolicyError(..)
200−
201− -- *** Post Policy condition helpers
202− , PostPolicyCondition
203− , ppCondBucket
204− , ppCondContentLengthRange
205− , ppCondContentType
206− , ppCondKey
207− , ppCondKeyStartsWith
208− , ppCondSuccessActionStatus
209−
210− -- * Error handling
211− -- | Data types representing various errors that may occur while
212− -- working with an object storage service.
213− , MinioErr(..)
214− , MErrV(..)
215− , ServiceErr(..)
216−
217−) where
26+ ( -- * Credentials
27+ Credentials (..),
28+
29+ -- ** Credential providers
30+
31+ -- | Run actions that retrieve 'Credentials' from the environment or
32+ -- files or other custom sources.
33+ Provider,
34+ fromAWSConfigFile,
35+ fromAWSEnv,
36+ fromMinioEnv,
37+ findFirst,
38+
39+ -- * Connecting to object storage
40+ ConnectInfo,
41+ setRegion,
42+ setCreds,
43+ setCredsFrom,
44+ isConnectInfoSecure,
45+ disableTLSCertValidation,
46+ MinioConn,
47+ mkMinioConn,
48+
49+ -- ** Connection helpers
50+
51+ -- | These are helpers to construct 'ConnectInfo' values for common
52+ -- cases.
53+ minioPlayCI,
54+ awsCI,
55+ gcsCI,
56+
57+ -- * Minio Monad
58+ ----------------
59+
60+ -- | The Minio Monad provides connection-reuse, bucket-location
61+ -- caching, resource management and simpler error handling
62+ -- functionality. All actions on object storage are performed within
63+ -- this Monad.
64+ Minio,
65+ runMinioWith,
66+ runMinio,
67+ runMinioResWith,
68+ runMinioRes,
69+
70+ -- * Bucket Operations
71+
72+ -- ** Creation, removal and querying
73+ Bucket,
74+ makeBucket,
75+ removeBucket,
76+ bucketExists,
77+ Region,
78+ getLocation,
79+
80+ -- ** Listing buckets
81+ BucketInfo (..),
82+ listBuckets,
83+
84+ -- ** Listing objects
85+ listObjects,
86+ listObjectsV1,
87+ ListItem (..),
88+ ObjectInfo,
89+ oiObject,
90+ oiModTime,
91+ oiETag,
92+ oiSize,
93+ oiUserMetadata,
94+ oiMetadata,
95+
96+ -- ** Listing incomplete uploads
97+ listIncompleteUploads,
98+ UploadId,
99+ UploadInfo (..),
100+ listIncompleteParts,
101+ ObjectPartInfo (..),
102+
103+ -- ** Bucket Notifications
104+ getBucketNotification,
105+ putBucketNotification,
106+ removeAllBucketNotification,
107+ Notification (..),
108+ defaultNotification,
109+ NotificationConfig (..),
110+ Arn,
111+ Event (..),
112+ Filter (..),
113+ defaultFilter,
114+ FilterKey (..),
115+ defaultFilterKey,
116+ FilterRules (..),
117+ defaultFilterRules,
118+ FilterRule (..),
119+
120+ -- * Object Operations
121+ Object,
122+
123+ -- ** File-based operations
124+ fGetObject,
125+ fPutObject,
126+
127+ -- ** Conduit-based streaming operations
128+ putObject,
129+ PutObjectOptions,
130+ defaultPutObjectOptions,
131+ pooContentType,
132+ pooContentEncoding,
133+ pooContentDisposition,
134+ pooContentLanguage,
135+ pooCacheControl,
136+ pooStorageClass,
137+ pooUserMetadata,
138+ pooNumThreads,
139+ pooSSE,
140+ getObject,
141+ GetObjectOptions,
142+ defaultGetObjectOptions,
143+ gooRange,
144+ gooIfMatch,
145+ gooIfNoneMatch,
146+ gooIfModifiedSince,
147+ gooIfUnmodifiedSince,
148+ gooSSECKey,
149+ GetObjectResponse,
150+ gorObjectInfo,
151+ gorObjectStream,
152+
153+ -- ** Server-side object copying
154+ copyObject,
155+ SourceInfo,
156+ defaultSourceInfo,
157+ srcBucket,
158+ srcObject,
159+ srcRange,
160+ srcIfMatch,
161+ srcIfNoneMatch,
162+ srcIfModifiedSince,
163+ srcIfUnmodifiedSince,
164+ DestinationInfo,
165+ defaultDestinationInfo,
166+ dstBucket,
167+ dstObject,
168+
169+ -- ** Querying object info
170+ statObject,
171+
172+ -- ** Object removal operations
173+ removeObject,
174+ removeIncompleteUpload,
175+
176+ -- ** Select Object Content with SQL
177+ module Network.Minio.SelectAPI,
178+
179+ -- * Server-Side Encryption Helpers
180+ mkSSECKey,
181+ SSECKey,
182+ SSE (..),
183+
184+ -- * Presigned Operations
185+ presignedPutObjectUrl,
186+ presignedGetObjectUrl,
187+ presignedHeadObjectUrl,
188+ UrlExpiry,
189+
190+ -- ** POST (browser) upload helpers
191+
192+ -- | Please see
193+ -- https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
194+ -- for detailed information.
195+ newPostPolicy,
196+ presignedPostPolicy,
197+ showPostPolicy,
198+ PostPolicy,
199+ PostPolicyError (..),
200+
201+ -- *** Post Policy condition helpers
202+ PostPolicyCondition,
203+ ppCondBucket,
204+ ppCondContentLengthRange,
205+ ppCondContentType,
206+ ppCondKey,
207+ ppCondKeyStartsWith,
208+ ppCondSuccessActionStatus,
209+
210+ -- * Error handling
211+
212+ -- | Data types representing various errors that may occur while
213+ -- working with an object storage service.
214+ MinioErr (..),
215+ MErrV (..),
216+ ServiceErr (..),
217+ )
218+where
218219
219220{-
220221This module exports the high-level MinIO API for object storage.
221222-}
222223
223−import qualified Data.Conduit as C
224−import qualified Data.Conduit.Binary as CB
224+import qualified Data.Conduit as C
225+import qualified Data.Conduit.Binary as CB
225226import qualified Data.Conduit.Combinators as CC
226−
227−import Lib.Prelude
228−
229−import Network.Minio.CopyObject
230−import Network.Minio.Data
231−import Network.Minio.Errors
232−import Network.Minio.ListOps
233−import Network.Minio.PutObject
234−import Network.Minio.S3API
235−import Network.Minio.SelectAPI
236−import Network.Minio.Utils
227+import Lib.Prelude
228+import Network.Minio.CopyObject
229+import Network.Minio.Data
230+import Network.Minio.Errors
231+import Network.Minio.ListOps
232+import Network.Minio.PutObject
233+import Network.Minio.S3API
234+import Network.Minio.SelectAPI
235+import Network.Minio.Utils
237236
238237-- | Lists buckets.
239238listBuckets :: Minio [BucketInfo]
@@ -248,8 +247,12 @@ fGetObject bucket object fp opts = do
248247 C.connect (gorObjectStream src) $ CB.sinkFileCautious fp
249248
250249-- | Upload the given file to the given object.
251−fPutObject :: Bucket -> Object -> FilePath
252− -> PutObjectOptions -> Minio ()
250+fPutObject ::
251+ Bucket ->
252+ Object ->
253+ FilePath ->
254+ PutObjectOptions ->
255+ Minio ()
253256fPutObject bucket object f opts =
254257 void $ putObjectInternal bucket object opts $ ODFile f Nothing
255258
@@ -257,8 +260,13 @@ fPutObject bucket object f opts =
257260-- known; this helps the library select optimal part sizes to perform
258261-- a multipart upload. If not specified, it is assumed that the object
259262-- can be potentially 5TiB and selects multipart sizes appropriately.
260−putObject :: Bucket -> Object -> C.ConduitM () ByteString Minio ()
261− -> Maybe Int64 -> PutObjectOptions -> Minio ()
263+putObject ::
264+ Bucket ->
265+ Object ->
266+ C.ConduitM () ByteString Minio () ->
267+ Maybe Int64 ->
268+ PutObjectOptions ->
269+ Minio ()
262270putObject bucket object src sizeMay opts =
263271 void $ putObjectInternal bucket object opts $ ODStream src sizeMay
264272
@@ -268,18 +276,25 @@ putObject bucket object src sizeMay opts =
268276-- copy operation if the new object is to be greater than 5GiB in
269277-- size.
270278copyObject :: DestinationInfo -> SourceInfo -> Minio ()
271−copyObject dstInfo srcInfo = void $ copyObjectInternal (dstBucket dstInfo)
272− (dstObject dstInfo) srcInfo
279+copyObject dstInfo srcInfo =
280+ void $
281+ copyObjectInternal
282+ (dstBucket dstInfo)
283+ (dstObject dstInfo)
284+ srcInfo
273285
274286-- | Remove an object from the object store.
275287removeObject :: Bucket -> Object -> Minio ()
276288removeObject = deleteObject
277289
278290-- | Get an object from the object store.
279−getObject :: Bucket -> Object -> GetObjectOptions
280− -> Minio GetObjectResponse
291+getObject ::
292+ Bucket ->
293+ Object ->
294+ GetObjectOptions ->
295+ Minio GetObjectResponse
281296getObject bucket object opts =
282− getObject' bucket object [] $ gooToHeaders opts
297+ getObject' bucket object [] $ gooToHeaders opts
283298
284299-- | Get an object's metadata from the object store. It accepts the
285300-- same options as GetObject.
@@ -309,6 +324,8 @@ bucketExists = headBucket
309324-- | Removes an ongoing multipart upload of an object.
310325removeIncompleteUpload :: Bucket -> Object -> Minio ()
311326removeIncompleteUpload bucket object = do
312− uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
313− C..| CC.sinkList
327+ uploads <-
328+ C.runConduit $
329+ listIncompleteUploads bucket (Just object) False
330+ C..| CC.sinkList
314331 mapM_ (abortMultipartUpload bucket object) (uiUploadId <$> uploads)
src/Network/Minio/API.hs+183−156
@@ -15,169 +15,187 @@
1515--
1616
1717module Network.Minio.API
18− ( connect
19− , S3ReqInfo(..)
20− , runMinio
21− , executeRequest
22− , mkStreamRequest
23− , getLocation
24−
25− , isValidBucketName
26− , checkBucketNameValidity
27− , isValidObjectName
28− , checkObjectNameValidity
29− ) where
30−
31−import Control.Retry (fullJitterBackoff,
32− limitRetriesByCumulativeDelay,
33− retrying)
34−import qualified Data.ByteString as B
35−import qualified Data.Char as C
36−import qualified Data.Conduit as C
37−import qualified Data.HashMap.Strict as H
38−import qualified Data.Text as T
39−import qualified Data.Time.Clock as Time
40−import Network.HTTP.Conduit (Response)
41−import qualified Network.HTTP.Conduit as NC
42−import qualified Network.HTTP.Types as HT
43−import Network.HTTP.Types.Header (hHost)
44−
45−import Lib.Prelude
46−
47−import Network.Minio.APICommon
48−import Network.Minio.Data
49−import Network.Minio.Errors
50−import Network.Minio.Sign.V4
51−import Network.Minio.Utils
52−import Network.Minio.XmlParser
18+ ( connect,
19+ S3ReqInfo (..),
20+ runMinio,
21+ executeRequest,
22+ mkStreamRequest,
23+ getLocation,
24+ isValidBucketName,
25+ checkBucketNameValidity,
26+ isValidObjectName,
27+ checkObjectNameValidity,
28+ )
29+where
30+
31+import Control.Retry
32+ ( fullJitterBackoff,
33+ limitRetriesByCumulativeDelay,
34+ retrying,
35+ )
36+import qualified Data.ByteString as B
37+import qualified Data.Char as C
38+import qualified Data.Conduit as C
39+import qualified Data.HashMap.Strict as H
40+import qualified Data.Text as T
41+import qualified Data.Time.Clock as Time
42+import Lib.Prelude
43+import Network.HTTP.Conduit (Response)
44+import qualified Network.HTTP.Conduit as NC
45+import qualified Network.HTTP.Types as HT
46+import Network.HTTP.Types.Header (hHost)
47+import Network.Minio.APICommon
48+import Network.Minio.Data
49+import Network.Minio.Errors
50+import Network.Minio.Sign.V4
51+import Network.Minio.Utils
52+import Network.Minio.XmlParser
5353
5454-- | Fetch bucket location (region)
5555getLocation :: Bucket -> Minio Region
5656getLocation bucket = do
57− resp <- executeRequest $ defaultS3ReqInfo {
58− riBucket = Just bucket
59− , riQueryParams = [("location", Nothing)]
60− , riNeedsLocation = False
61− }
57+ resp <-
58+ executeRequest $
59+ defaultS3ReqInfo
60+ { riBucket = Just bucket,
61+ riQueryParams = [("location", Nothing)],
62+ riNeedsLocation = False
63+ }
6264 parseLocation $ NC.responseBody resp
6365
64−
6566-- | Looks for region in RegionMap and updates it using getLocation if
6667-- absent.
6768discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
6869discoverRegion ri = runMaybeT $ do
6970 bucket <- MaybeT $ return $ riBucket ri
7071 regionMay <- lift $ lookupRegionCache bucket
71− maybe (do
72− l <- lift $ getLocation bucket
73− lift $ addToRegionCache bucket l
74− return l
75− ) return regionMay
72+ maybe
73+ ( do
74+ l <- lift $ getLocation bucket
75+ lift $ addToRegionCache bucket l
76+ return l
77+ )
78+ return
79+ regionMay
7680
7781getRegion :: S3ReqInfo -> Minio (Maybe Region)
7882getRegion ri = do
79− ci <- asks mcConnInfo
80−
81− -- getService/makeBucket/getLocation -- don't need location
82− if | not $ riNeedsLocation ri ->
83− return $ Just $ connectRegion ci
84−
85− -- if autodiscovery of location is disabled by user
86− | not $ connectAutoDiscoverRegion ci ->
87− return $ Just $ connectRegion ci
88−
89− -- discover the region for the request
90− | otherwise -> discoverRegion ri
83+ ci <- asks mcConnInfo
84+
85+ -- getService/makeBucket/getLocation -- don't need location
86+ if
87+ | not $ riNeedsLocation ri ->
88+ return $ Just $ connectRegion ci
89+ -- if autodiscovery of location is disabled by user
90+ | not $ connectAutoDiscoverRegion ci ->
91+ return $ Just $ connectRegion ci
92+ -- discover the region for the request
93+ | otherwise -> discoverRegion ri
9194
9295getRegionHost :: Region -> Minio Text
9396getRegionHost r = do
94− ci <- asks mcConnInfo
95−
96− if "amazonaws.com" `T.isSuffixOf` connectHost ci
97− then maybe (throwIO $ MErrVRegionNotSupported r)
98− return (H.lookup r awsRegionMap)
97+ ci <- asks mcConnInfo
98+
99+ if "amazonaws.com" `T.isSuffixOf` connectHost ci
100+ then
101+ maybe
102+ (throwIO $ MErrVRegionNotSupported r)
103+ return
104+ (H.lookup r awsRegionMap)
99105 else return $ connectHost ci
100106
101107buildRequest :: S3ReqInfo -> Minio NC.Request
102108buildRequest ri = do
103− maybe (return ()) checkBucketNameValidity $ riBucket ri
104− maybe (return ()) checkObjectNameValidity $ riObject ri
105−
106− ci <- asks mcConnInfo
107−
108− regionMay <- getRegion ri
109−
110− regionHost <- maybe (return $ connectHost ci) getRegionHost regionMay
111−
112− let ri' = ri { riHeaders = hostHeader : riHeaders ri
113− , riRegion = regionMay
114− }
115− ci' = ci { connectHost = regionHost }
116− hostHeader = (hHost, getHostAddr ci')
117−
118− -- Does not contain body and auth info.
119− baseRequest = NC.defaultRequest
120− { NC.method = riMethod ri'
121− , NC.secure = connectIsSecure ci'
122− , NC.host = encodeUtf8 $ connectHost ci'
123− , NC.port = connectPort ci'
124− , NC.path = getS3Path (riBucket ri') (riObject ri')
125− , NC.requestHeaders = riHeaders ri'
126− , NC.queryString = HT.renderQuery False $ riQueryParams ri'
127− }
128−
129− timeStamp <- liftIO Time.getCurrentTime
130−
131− let sp = SignParams (connectAccessKey ci') (connectSecretKey ci')
132− timeStamp (riRegion ri') Nothing Nothing
133−
134− -- Cases to handle:
135− --
136− -- 1. Connection is secure: use unsigned payload
137− --
138− -- 2. Insecure connection, streaming signature is enabled via use of
139− -- conduit payload: use streaming signature for request.
140− --
141− -- 3. Insecure connection, non-conduit payload: compute payload
142− -- sha256hash, buffer request in memory and perform request.
143−
144− -- case 2 from above.
145− if | isStreamingPayload (riPayload ri') &&
146− (not $ connectIsSecure ci') -> do
147− (pLen, pSrc) <- case riPayload ri of
148− PayloadC l src -> return (l, src)
149− _ -> throwIO MErrVUnexpectedPayload
150− let reqFn = signV4Stream pLen sp baseRequest
151− return $ reqFn pSrc
152−
153− | otherwise -> do
154− -- case 1 described above.
155− sp' <- if | connectIsSecure ci' -> return sp
156− -- case 3 described above.
157− | otherwise -> do
158− pHash <- getPayloadSHA256Hash $ riPayload ri'
159− return $ sp { spPayloadHash = Just pHash }
160−
161− let signHeaders = signV4 sp' baseRequest
162− return $ baseRequest
163− { NC.requestHeaders =
164− NC.requestHeaders baseRequest ++
165− mkHeaderFromPairs signHeaders
166− , NC.requestBody = getRequestBody (riPayload ri')
167− }
168−
109+ maybe (return ()) checkBucketNameValidity $ riBucket ri
110+ maybe (return ()) checkObjectNameValidity $ riObject ri
111+
112+ ci <- asks mcConnInfo
113+
114+ regionMay <- getRegion ri
115+
116+ regionHost <- maybe (return $ connectHost ci) getRegionHost regionMay
117+
118+ let ri' =
119+ ri
120+ { riHeaders = hostHeader : riHeaders ri,
121+ riRegion = regionMay
122+ }
123+ ci' = ci {connectHost = regionHost}
124+ hostHeader = (hHost, getHostAddr ci')
125+ -- Does not contain body and auth info.
126+ baseRequest =
127+ NC.defaultRequest
128+ { NC.method = riMethod ri',
129+ NC.secure = connectIsSecure ci',
130+ NC.host = encodeUtf8 $ connectHost ci',
131+ NC.port = connectPort ci',
132+ NC.path = getS3Path (riBucket ri') (riObject ri'),
133+ NC.requestHeaders = riHeaders ri',
134+ NC.queryString = HT.renderQuery False $ riQueryParams ri'
135+ }
136+
137+ timeStamp <- liftIO Time.getCurrentTime
138+
139+ let sp =
140+ SignParams
141+ (connectAccessKey ci')
142+ (connectSecretKey ci')
143+ timeStamp
144+ (riRegion ri')
145+ Nothing
146+ Nothing
147+
148+ -- Cases to handle:
149+ --
150+ -- 1. Connection is secure: use unsigned payload
151+ --
152+ -- 2. Insecure connection, streaming signature is enabled via use of
153+ -- conduit payload: use streaming signature for request.
154+ --
155+ -- 3. Insecure connection, non-conduit payload: compute payload
156+ -- sha256hash, buffer request in memory and perform request.
157+
158+ -- case 2 from above.
159+ if
160+ | isStreamingPayload (riPayload ri')
161+ && (not $ connectIsSecure ci') -> do
162+ (pLen, pSrc) <- case riPayload ri of
163+ PayloadC l src -> return (l, src)
164+ _ -> throwIO MErrVUnexpectedPayload
165+ let reqFn = signV4Stream pLen sp baseRequest
166+ return $ reqFn pSrc
167+ | otherwise -> do
168+ -- case 1 described above.
169+ sp' <-
170+ if
171+ | connectIsSecure ci' -> return sp
172+ -- case 3 described above.
173+ | otherwise -> do
174+ pHash <- getPayloadSHA256Hash $ riPayload ri'
175+ return $ sp {spPayloadHash = Just pHash}
176+
177+ let signHeaders = signV4 sp' baseRequest
178+ return $
179+ baseRequest
180+ { NC.requestHeaders =
181+ NC.requestHeaders baseRequest
182+ ++ mkHeaderFromPairs signHeaders,
183+ NC.requestBody = getRequestBody (riPayload ri')
184+ }
169185
170186retryAPIRequest :: Minio a -> Minio a
171187retryAPIRequest apiCall = do
172− resE <- retrying retryPolicy (const shouldRetry) $
173− const $ try apiCall
188+ resE <-
189+ retrying retryPolicy (const shouldRetry)
190+ $ const
191+ $ try apiCall
174192 either throwIO return resE
175193 where
176194 -- Retry using the full-jitter backoff method for up to 10 mins
177195 -- total
178− retryPolicy = limitRetriesByCumulativeDelay tenMins
179− $ fullJitterBackoff oneMilliSecond
180−
196+ retryPolicy =
197+ limitRetriesByCumulativeDelay tenMins $
198+ fullJitterBackoff oneMilliSecond
181199 oneMilliSecond = 1000 -- in microseconds
182200 tenMins = 10 * 60 * 1000000 -- in microseconds
183201 -- retry on connection related failure
@@ -189,23 +207,23 @@ retryAPIRequest apiCall = do
189207 -- API request failed with a retryable exception
190208 Left httpExn@(NC.HttpExceptionRequest _ exn) ->
191209 case (exn :: NC.HttpExceptionContent) of
192− NC.ResponseTimeout -> return True
193− NC.ConnectionTimeout -> return True
210+ NC.ResponseTimeout -> return True
211+ NC.ConnectionTimeout -> return True
194212 NC.ConnectionFailure _ -> return True
195213 -- We received an unexpected exception
196− _ -> throwIO httpExn
214+ _ -> throwIO httpExn
197215 -- We received an unexpected exception
198216 Left someOtherExn -> throwIO someOtherExn
199217
200−
201218executeRequest :: S3ReqInfo -> Minio (Response LByteString)
202219executeRequest ri = do
203220 req <- buildRequest ri
204221 mgr <- asks mcConnManager
205222 retryAPIRequest $ httpLbs req mgr
206223
207−mkStreamRequest :: S3ReqInfo
208− -> Minio (Response (C.ConduitM () ByteString Minio ()))
224+mkStreamRequest ::
225+ S3ReqInfo ->
226+ Minio (Response (C.ConduitM () ByteString Minio ()))
209227mkStreamRequest ri = do
210228 req <- buildRequest ri
211229 mgr <- asks mcConnManager
@@ -214,35 +232,43 @@ mkStreamRequest ri = do
214232-- Bucket name validity check according to AWS rules.
215233isValidBucketName :: Bucket -> Bool
216234isValidBucketName bucket =
217− not (or [ len < 3 || len > 63
218− , or (map labelCheck labels)
219− , or (map labelCharsCheck labels)
220− , isIPCheck
221− ])
235+ not
236+ ( or
237+ [ len < 3 || len > 63,
238+ or (map labelCheck labels),
239+ or (map labelCharsCheck labels),
240+ isIPCheck
241+ ]
242+ )
222243 where
223244 len = T.length bucket
224245 labels = T.splitOn "." bucket
225−
226246 -- does label `l` fail basic checks of length and start/end?
227247 labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
228−
229248 -- does label `l` have non-allowed characters?
230− labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||
231− x == '-' ||
232− C.isDigit x)) l
233−
249+ labelCharsCheck l =
250+ isJust $
251+ T.find
252+ ( \x ->
253+ not
254+ ( C.isAsciiLower x
255+ || x == '-'
256+ || C.isDigit x
257+ )
258+ )
259+ l
234260 -- does label `l` have non-digit characters?
235261 labelNonDigits l = isJust $ T.find (not . C.isDigit) l
236262 labelAsNums = map (not . labelNonDigits) labels
237−
238263 -- check if bucket name looks like an IP
239264 isIPCheck = and labelAsNums && length labelAsNums == 4
240265
241266-- Throws exception iff bucket name is invalid according to AWS rules.
242267checkBucketNameValidity :: MonadIO m => Bucket -> m ()
243268checkBucketNameValidity bucket =
244− when (not $ isValidBucketName bucket) $
245− throwIO $ MErrVInvalidBucketName bucket
269+ when (not $ isValidBucketName bucket)
270+ $ throwIO
271+ $ MErrVInvalidBucketName bucket
246272
247273isValidObjectName :: Object -> Bool
248274isValidObjectName object =
@@ -250,5 +276,6 @@ isValidObjectName object =
250276
251277checkObjectNameValidity :: MonadIO m => Object -> m ()
252278checkObjectNameValidity object =
253− when (not $ isValidObjectName object) $
254− throwIO $ MErrVInvalidObjectName object
279+ when (not $ isValidObjectName object)
280+ $ throwIO
281+ $ MErrVInvalidObjectName object
src/Network/Minio/APICommon.hs+27−23
@@ -16,37 +16,38 @@
1616
1717module Network.Minio.APICommon where
1818
19−import qualified Conduit as C
20−import qualified Data.ByteString as BS
21−import qualified Data.ByteString.Lazy as LB
22−import Data.Conduit.Binary (sourceHandleRange)
23−import qualified Network.HTTP.Conduit as NC
24−import qualified Network.HTTP.Types as HT
25−
26−import Lib.Prelude
27−
28−import Network.Minio.Data
29−import Network.Minio.Data.Crypto
30−import Network.Minio.Errors
19+import qualified Conduit as C
20+import qualified Data.ByteString as BS
21+import qualified Data.ByteString.Lazy as LB
22+import Data.Conduit.Binary (sourceHandleRange)
23+import Lib.Prelude
24+import qualified Network.HTTP.Conduit as NC
25+import qualified Network.HTTP.Types as HT
26+import Network.Minio.Data
27+import Network.Minio.Data.Crypto
28+import Network.Minio.Errors
3129
3230sha256Header :: ByteString -> HT.Header
33−sha256Header = ("x-amz-content-sha256", )
31+sha256Header = ("x-amz-content-sha256",)
3432
3533-- | This function throws an error if the payload is a conduit (as it
3634-- will not be possible to re-read the conduit after it is consumed).
3735getPayloadSHA256Hash :: Payload -> Minio ByteString
3836getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
39−getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $
40− sourceHandleRange h
41− (return . fromIntegral $ off)
42− (return . fromIntegral $ size)
37+getPayloadSHA256Hash (PayloadH h off size) =
38+ hashSHA256FromSource $
39+ sourceHandleRange
40+ h
41+ (return . fromIntegral $ off)
42+ (return . fromIntegral $ size)
4343getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
4444
4545getRequestBody :: Payload -> NC.RequestBody
4646getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
4747getRequestBody (PayloadH h off size) =
4848 NC.requestBodySource (fromIntegral size) $
49− sourceHandleRange h
49+ sourceHandleRange
50+ h
5051 (return . fromIntegral $ off)
5152 (return . fromIntegral $ size)
5253getRequestBody (PayloadC n src) = NC.requestBodySource n src
@@ -55,14 +56,17 @@ mkStreamingPayload :: Payload -> Payload
5556mkStreamingPayload payload =
5657 case payload of
5758 PayloadBS bs ->
58− PayloadC (fromIntegral $ BS.length bs)
59+ PayloadC
60+ (fromIntegral $ BS.length bs)
5961 (C.sourceLazy $ LB.fromStrict bs)
6062 PayloadH h off len ->
61− PayloadC len $ sourceHandleRange h
62− (return . fromIntegral $ off)
63− (return . fromIntegral $ len)
63+ PayloadC len $
64+ sourceHandleRange
65+ h
66+ (return . fromIntegral $ off)
67+ (return . fromIntegral $ len)
6468 _ -> payload
6569
6670isStreamingPayload :: Payload -> Bool
6771isStreamingPayload (PayloadC _ _) = True
68−isStreamingPayload _ = False
72+isStreamingPayload _ = False
src/Network/Minio/AdminAPI.hs+470−384
@@ -16,270 +16,304 @@
1616
1717module Network.Minio.AdminAPI
1818 ( -- * MinIO Admin API
19− --------------------
19+ --------------------
20+
2021 -- | Provides MinIO admin API and related types. It is in
2122 -- experimental state.
22− DriveInfo(..)
23− , ErasureInfo(..)
24− , Backend(..)
25− , ConnStats(..)
26− , HttpStats(..)
27− , ServerProps(..)
28− , CountNAvgTime(..)
29− , StorageClass(..)
30− , StorageInfo(..)
31− , SIData(..)
32− , ServerInfo(..)
33− , getServerInfo
34−
35− , HealOpts(..)
36− , HealResultItem(..)
37− , HealStatus(..)
38− , HealStartResp(..)
39− , startHeal
40− , forceStartHeal
41− , getHealStatus
42−
43− , SetConfigResult(..)
44− , NodeSummary(..)
45− , setConfig
46− , getConfig
47−
48− , ServerVersion(..)
49− , ServiceStatus(..)
50− , serviceStatus
51−
52− , ServiceAction(..)
53− , serviceSendAction
54− ) where
55−
56−import Data.Aeson (FromJSON, ToJSON, Value (Object),
57− eitherDecode, object, pairs,
58− parseJSON, toEncoding, toJSON,
59− withObject, withText, (.:), (.:?),
60− (.=))
61−import qualified Data.Aeson as A
62−import Data.Aeson.Types (typeMismatch)
63−import qualified Data.ByteString as B
64−import qualified Data.ByteString.Lazy as LBS
65−import qualified Data.Text as T
66−import Data.Time (NominalDiffTime, getCurrentTime)
67−import Network.HTTP.Conduit (Response)
68−import qualified Network.HTTP.Conduit as NC
69−import qualified Network.HTTP.Types as HT
70−import Network.HTTP.Types.Header (hHost)
71−
72−import Lib.Prelude
73−
74−import Network.Minio.APICommon
75−import Network.Minio.Data
76−import Network.Minio.Errors
77−import Network.Minio.Sign.V4
78−import Network.Minio.Utils
23+ DriveInfo (..),
24+ ErasureInfo (..),
25+ Backend (..),
26+ ConnStats (..),
27+ HttpStats (..),
28+ ServerProps (..),
29+ CountNAvgTime (..),
30+ StorageClass (..),
31+ StorageInfo (..),
32+ SIData (..),
33+ ServerInfo (..),
34+ getServerInfo,
35+ HealOpts (..),
36+ HealResultItem (..),
37+ HealStatus (..),
38+ HealStartResp (..),
39+ startHeal,
40+ forceStartHeal,
41+ getHealStatus,
42+ SetConfigResult (..),
43+ NodeSummary (..),
44+ setConfig,
45+ getConfig,
46+ ServerVersion (..),
47+ ServiceStatus (..),
48+ serviceStatus,
49+ ServiceAction (..),
50+ serviceSendAction,
51+ )
52+where
53+
54+import Data.Aeson
55+ ( (.:),
56+ (.:?),
57+ (.=),
58+ FromJSON,
59+ ToJSON,
60+ Value (Object),
61+ eitherDecode,
62+ object,
63+ pairs,
64+ parseJSON,
65+ toEncoding,
66+ toJSON,
67+ withObject,
68+ withText,
69+ )
70+import qualified Data.Aeson as A
71+import Data.Aeson.Types (typeMismatch)
72+import qualified Data.ByteString as B
73+import qualified Data.ByteString.Lazy as LBS
74+import qualified Data.Text as T
75+import Data.Time (NominalDiffTime, getCurrentTime)
76+import Lib.Prelude
77+import Network.HTTP.Conduit (Response)
78+import qualified Network.HTTP.Conduit as NC
79+import qualified Network.HTTP.Types as HT
80+import Network.HTTP.Types.Header (hHost)
81+import Network.Minio.APICommon
82+import Network.Minio.Data
83+import Network.Minio.Errors
84+import Network.Minio.Sign.V4
85+import Network.Minio.Utils
7986
8087data DriveInfo = DriveInfo
81− { diUuid :: Text
82− , diEndpoint :: Text
83− , diState :: Text
84− } deriving (Eq, Show)
88+ { diUuid :: Text,
89+ diEndpoint :: Text,
90+ diState :: Text
91+ }
92+ deriving (Eq, Show)
8593
8694instance FromJSON DriveInfo where
87− parseJSON = withObject "DriveInfo" $ \v -> DriveInfo
88− <$> v .: "uuid"
89− <*> v .: "endpoint"
90− <*> v .: "state"
95+ parseJSON = withObject "DriveInfo" $ \v ->
96+ DriveInfo
97+ <$> v .: "uuid"
98+ <*> v .: "endpoint"
99+ <*> v .: "state"
91100
92101data StorageClass = StorageClass
93− { scParity :: Int
94− , scData :: Int
95− } deriving (Eq, Show)
102+ { scParity :: Int,
103+ scData :: Int
104+ }
105+ deriving (Eq, Show)
96106
97107data ErasureInfo = ErasureInfo
98− { eiOnlineDisks :: Int
99− , eiOfflineDisks :: Int
100− , eiStandard :: StorageClass
101− , eiReducedRedundancy :: StorageClass
102− , eiSets :: [[DriveInfo]]
103− } deriving (Eq, Show)
108+ { eiOnlineDisks :: Int,
109+ eiOfflineDisks :: Int,
110+ eiStandard :: StorageClass,
111+ eiReducedRedundancy :: StorageClass,
112+ eiSets :: [[DriveInfo]]
113+ }
114+ deriving (Eq, Show)
104115
105116instance FromJSON ErasureInfo where
106− parseJSON = withObject "ErasureInfo" $ \v -> do
107− onlineDisks <- v .: "OnlineDisks"
108− offlineDisks <- v .: "OfflineDisks"
109− stdClass <- StorageClass
110− <$> v .: "StandardSCData"
111− <*> v .: "StandardSCParity"
112− rrClass <- StorageClass
113− <$> v .: "RRSCData"
114− <*> v .: "RRSCParity"
115− sets <- v .: "Sets"
116− return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
117−
118−data Backend = BackendFS
119− | BackendErasure ErasureInfo
120− deriving (Eq, Show)
117+ parseJSON = withObject "ErasureInfo" $ \v -> do
118+ onlineDisks <- v .: "OnlineDisks"
119+ offlineDisks <- v .: "OfflineDisks"
120+ stdClass <-
121+ StorageClass
122+ <$> v .: "StandardSCData"
123+ <*> v .: "StandardSCParity"
124+ rrClass <-
125+ StorageClass
126+ <$> v .: "RRSCData"
127+ <*> v .: "RRSCParity"
128+ sets <- v .: "Sets"
129+ return $ ErasureInfo onlineDisks offlineDisks stdClass rrClass sets
130+
131+data Backend
132+ = BackendFS
133+ | BackendErasure ErasureInfo
134+ deriving (Eq, Show)
121135
122136instance FromJSON Backend where
123− parseJSON = withObject "Backend" $ \v -> do
124− typ <- v .: "Type"
125− case typ :: Int of
126− 1 -> return BackendFS
127− 2 -> BackendErasure <$> parseJSON (Object v)
128− _ -> typeMismatch "BackendType" (Object v)
137+ parseJSON = withObject "Backend" $ \v -> do
138+ typ <- v .: "Type"
139+ case typ :: Int of
140+ 1 -> return BackendFS
141+ 2 -> BackendErasure <$> parseJSON (Object v)
142+ _ -> typeMismatch "BackendType" (Object v)
129143
130144data ConnStats = ConnStats
131− { csTransferred :: Int64
132− , csReceived :: Int64
133− } deriving (Eq, Show)
145+ { csTransferred :: Int64,
146+ csReceived :: Int64
147+ }
148+ deriving (Eq, Show)
134149
135150instance FromJSON ConnStats where
136− parseJSON = withObject "ConnStats" $ \v -> ConnStats
137− <$> v .: "transferred"
138− <*> v .: "received"
151+ parseJSON = withObject "ConnStats" $ \v ->
152+ ConnStats
153+ <$> v .: "transferred"
154+ <*> v .: "received"
139155
140156data ServerProps = ServerProps
141− { spUptime :: NominalDiffTime
142− , spVersion :: Text
143− , spCommitId :: Text
144− , spRegion :: Text
145− , spSqsArns :: [Text]
146− } deriving (Eq, Show)
157+ { spUptime :: NominalDiffTime,
158+ spVersion :: Text,
159+ spCommitId :: Text,
160+ spRegion :: Text,
161+ spSqsArns :: [Text]
162+ }
163+ deriving (Eq, Show)
147164
148165instance FromJSON ServerProps where
149− parseJSON = withObject "SIServer" $ \v -> do
150− uptimeNs <- v .: "uptime"
151− let uptime = uptimeNs / 1e9
152− ver <- v .: "version"
153− commitId <- v .: "commitID"
154− region <- v .: "region"
155− arn <- v .: "sqsARN"
156− return $ ServerProps uptime ver commitId region arn
166+ parseJSON = withObject "SIServer" $ \v -> do
167+ uptimeNs <- v .: "uptime"
168+ let uptime = uptimeNs / 1e9
169+ ver <- v .: "version"
170+ commitId <- v .: "commitID"
171+ region <- v .: "region"
172+ arn <- v .: "sqsARN"
173+ return $ ServerProps uptime ver commitId region arn
157174
158175data StorageInfo = StorageInfo
159− { siUsed :: Int64
160− , siBackend :: Backend
161− } deriving (Eq, Show)
176+ { siUsed :: Int64,
177+ siBackend :: Backend
178+ }
179+ deriving (Eq, Show)
162180
163181instance FromJSON StorageInfo where
164− parseJSON = withObject "StorageInfo" $ \v -> StorageInfo
165− <$> v .: "Used"
166− <*> v .: "Backend"
182+ parseJSON = withObject "StorageInfo" $ \v ->
183+ StorageInfo
184+ <$> v .: "Used"
185+ <*> v .: "Backend"
167186
168187data CountNAvgTime = CountNAvgTime
169− { caCount :: Int64
170− , caAvgDuration :: Text
171− } deriving (Eq, Show)
188+ { caCount :: Int64,
189+ caAvgDuration :: Text
190+ }
191+ deriving (Eq, Show)
172192
173193instance FromJSON CountNAvgTime where
174− parseJSON = withObject "CountNAvgTime" $ \v -> CountNAvgTime
175− <$> v .: "count"
176− <*> v .: "avgDuration"
194+ parseJSON = withObject "CountNAvgTime" $ \v ->
195+ CountNAvgTime
196+ <$> v .: "count"
197+ <*> v .: "avgDuration"
177198
178199data HttpStats = HttpStats
179− { hsTotalHeads :: CountNAvgTime
180− , hsSuccessHeads :: CountNAvgTime
181− , hsTotalGets :: CountNAvgTime
182− , hsSuccessGets :: CountNAvgTime
183− , hsTotalPuts :: CountNAvgTime
184− , hsSuccessPuts :: CountNAvgTime
185− , hsTotalPosts :: CountNAvgTime
186− , hsSuccessPosts :: CountNAvgTime
187− , hsTotalDeletes :: CountNAvgTime
188− , hsSuccessDeletes :: CountNAvgTime
189− } deriving (Eq, Show)
200+ { hsTotalHeads :: CountNAvgTime,
201+ hsSuccessHeads :: CountNAvgTime,
202+ hsTotalGets :: CountNAvgTime,
203+ hsSuccessGets :: CountNAvgTime,
204+ hsTotalPuts :: CountNAvgTime,
205+ hsSuccessPuts :: CountNAvgTime,
206+ hsTotalPosts :: CountNAvgTime,
207+ hsSuccessPosts :: CountNAvgTime,
208+ hsTotalDeletes :: CountNAvgTime,
209+ hsSuccessDeletes :: CountNAvgTime
210+ }
211+ deriving (Eq, Show)
190212
191213instance FromJSON HttpStats where
192− parseJSON = withObject "HttpStats" $ \v -> HttpStats
193− <$> v .: "totalHEADs"
194− <*> v .: "successHEADs"
195− <*> v .: "totalGETs"
196− <*> v .: "successGETs"
197− <*> v .: "totalPUTs"
198− <*> v .: "successPUTs"
199− <*> v .: "totalPOSTs"
200− <*> v .: "successPOSTs"
201− <*> v .: "totalDELETEs"
202− <*> v .: "successDELETEs"
214+ parseJSON = withObject "HttpStats" $ \v ->
215+ HttpStats
216+ <$> v .: "totalHEADs"
217+ <*> v .: "successHEADs"
218+ <*> v .: "totalGETs"
219+ <*> v .: "successGETs"
220+ <*> v .: "totalPUTs"
221+ <*> v .: "successPUTs"
222+ <*> v .: "totalPOSTs"
223+ <*> v .: "successPOSTs"
224+ <*> v .: "totalDELETEs"
225+ <*> v .: "successDELETEs"
203226
204227data SIData = SIData
205− { sdStorage :: StorageInfo
206− , sdConnStats :: ConnStats
207− , sdHttpStats :: HttpStats
208− , sdProps :: ServerProps
209− } deriving (Eq, Show)
228+ { sdStorage :: StorageInfo,
229+ sdConnStats :: ConnStats,
230+ sdHttpStats :: HttpStats,
231+ sdProps :: ServerProps
232+ }
233+ deriving (Eq, Show)
210234
211235instance FromJSON SIData where
212− parseJSON = withObject "SIData" $ \v -> SIData
213− <$> v .: "storage"
214− <*> v .: "network"
215− <*> v .: "http"
216− <*> v .: "server"
236+ parseJSON = withObject "SIData" $ \v ->
237+ SIData
238+ <$> v .: "storage"
239+ <*> v .: "network"
240+ <*> v .: "http"
241+ <*> v .: "server"
217242
218243data ServerInfo = ServerInfo
219− { siError :: Text
220− , siAddr :: Text
221− , siData :: SIData
222− } deriving (Eq, Show)
244+ { siError :: Text,
245+ siAddr :: Text,
246+ siData :: SIData
247+ }
248+ deriving (Eq, Show)
223249
224250instance FromJSON ServerInfo where
225− parseJSON = withObject "ServerInfo" $ \v -> ServerInfo
226− <$> v .: "error"
227− <*> v .: "addr"
228− <*> v .: "data"
251+ parseJSON = withObject "ServerInfo" $ \v ->
252+ ServerInfo
253+ <$> v .: "error"
254+ <*> v .: "addr"
255+ <*> v .: "data"
229256
230257data ServerVersion = ServerVersion
231− { svVersion :: Text
232− , svCommitId :: Text
233− } deriving (Eq, Show)
258+ { svVersion :: Text,
259+ svCommitId :: Text
260+ }
261+ deriving (Eq, Show)
234262
235263instance FromJSON ServerVersion where
236− parseJSON = withObject "ServerVersion" $ \v -> ServerVersion
264+ parseJSON = withObject "ServerVersion" $ \v ->
265+ ServerVersion
237266 <$> v .: "version"
238267 <*> v .: "commitID"
239268
240269data ServiceStatus = ServiceStatus
241− { ssVersion :: ServerVersion
242− , ssUptime :: NominalDiffTime
243− } deriving (Eq, Show)
270+ { ssVersion :: ServerVersion,
271+ ssUptime :: NominalDiffTime
272+ }
273+ deriving (Eq, Show)
244274
245275instance FromJSON ServiceStatus where
246− parseJSON = withObject "ServiceStatus" $ \v -> do
247− serverVersion <- v .: "serverVersion"
248− uptimeNs <- v .: "uptime"
249− let uptime = uptimeNs / 1e9
250− return $ ServiceStatus serverVersion uptime
276+ parseJSON = withObject "ServiceStatus" $ \v -> do
277+ serverVersion <- v .: "serverVersion"
278+ uptimeNs <- v .: "uptime"
279+ let uptime = uptimeNs / 1e9
280+ return $ ServiceStatus serverVersion uptime
251281
252−data ServiceAction = ServiceActionRestart
253− | ServiceActionStop
254− deriving (Eq, Show)
282+data ServiceAction
283+ = ServiceActionRestart
284+ | ServiceActionStop
285+ deriving (Eq, Show)
255286
256287instance ToJSON ServiceAction where
257− toJSON a = object [ "action" .= serviceActionToText a ]
288+ toJSON a = object ["action" .= serviceActionToText a]
258289
259290serviceActionToText :: ServiceAction -> Text
260291serviceActionToText a = case a of
261292 ServiceActionRestart -> "restart"
262− ServiceActionStop -> "stop"
293+ ServiceActionStop -> "stop"
263294
264295adminPath :: ByteString
265296adminPath = "/minio/admin"
266297
267298data HealStartResp = HealStartResp
268− { hsrClientToken :: Text
269− , hsrClientAddr :: Text
270− , hsrStartTime :: UTCTime
271− } deriving (Eq, Show)
299+ { hsrClientToken :: Text,
300+ hsrClientAddr :: Text,
301+ hsrStartTime :: UTCTime
302+ }
303+ deriving (Eq, Show)
272304
273305instance FromJSON HealStartResp where
274− parseJSON = withObject "HealStartResp" $ \v -> HealStartResp
275− <$> v .: "clientToken"
276− <*> v .: "clientAddress"
277− <*> v .: "startTime"
306+ parseJSON = withObject "HealStartResp" $ \v ->
307+ HealStartResp
308+ <$> v .: "clientToken"
309+ <*> v .: "clientAddress"
310+ <*> v .: "startTime"
278311
279312data HealOpts = HealOpts
280− { hoRecursive :: Bool
281− , hoDryRun :: Bool
282− } deriving (Eq, Show)
313+ { hoRecursive :: Bool,
314+ hoDryRun :: Bool
315+ }
316+ deriving (Eq, Show)
283317
284318instance ToJSON HealOpts where
285319 toJSON (HealOpts r d) =
@@ -288,197 +322,234 @@ instance ToJSON HealOpts where
288322 pairs ("recursive" .= r <> "dryRun" .= d)
289323
290324instance FromJSON HealOpts where
291− parseJSON = withObject "HealOpts" $ \v -> HealOpts
325+ parseJSON = withObject "HealOpts" $ \v ->
326+ HealOpts
292327 <$> v .: "recursive"
293328 <*> v .: "dryRun"
294329
295−data HealItemType = HealItemMetadata
296− | HealItemBucket
297− | HealItemBucketMetadata
298− | HealItemObject
299− deriving (Eq, Show)
330+data HealItemType
331+ = HealItemMetadata
332+ | HealItemBucket
333+ | HealItemBucketMetadata
334+ | HealItemObject
335+ deriving (Eq, Show)
300336
301337instance FromJSON HealItemType where
302− parseJSON = withText "HealItemType" $ \v -> case v of
303− "metadata" -> return HealItemMetadata
304− "bucket" -> return HealItemBucket
305− "object" -> return HealItemObject
306− "bucket-metadata" -> return HealItemBucketMetadata
307− _ -> typeMismatch "HealItemType" (A.String v)
338+ parseJSON = withText "HealItemType" $ \v -> case v of
339+ "metadata" -> return HealItemMetadata
340+ "bucket" -> return HealItemBucket
341+ "object" -> return HealItemObject
342+ "bucket-metadata" -> return HealItemBucketMetadata
343+ _ -> typeMismatch "HealItemType" (A.String v)
308344
309345data NodeSummary = NodeSummary
310− { nsName :: Text
311− , nsErrSet :: Bool
312− , nsErrMessage :: Text
313− } deriving (Eq, Show)
346+ { nsName :: Text,
347+ nsErrSet :: Bool,
348+ nsErrMessage :: Text
349+ }
350+ deriving (Eq, Show)
314351
315352instance FromJSON NodeSummary where
316− parseJSON = withObject "NodeSummary" $ \v -> NodeSummary
317− <$> v .: "name"
318− <*> v .: "errSet"
319− <*> v .: "errMsg"
353+ parseJSON = withObject "NodeSummary" $ \v ->
354+ NodeSummary
355+ <$> v .: "name"
356+ <*> v .: "errSet"
357+ <*> v .: "errMsg"
320358
321359data SetConfigResult = SetConfigResult
322− { scrStatus :: Bool
323− , scrNodeSummary :: [NodeSummary]
324− } deriving (Eq, Show)
360+ { scrStatus :: Bool,
361+ scrNodeSummary :: [NodeSummary]
362+ }
363+ deriving (Eq, Show)
325364
326365instance FromJSON SetConfigResult where
327− parseJSON = withObject "SetConfigResult" $ \v -> SetConfigResult
328− <$> v .: "status"
329− <*> v .: "nodeResults"
366+ parseJSON = withObject "SetConfigResult" $ \v ->
367+ SetConfigResult
368+ <$> v .: "status"
369+ <*> v .: "nodeResults"
330370
331371data HealResultItem = HealResultItem
332− { hriResultIdx :: Int
333− , hriType :: HealItemType
334− , hriBucket :: Bucket
335− , hriObject :: Object
336− , hriDetail :: Text
337− , hriParityBlocks :: Maybe Int
338− , hriDataBlocks :: Maybe Int
339− , hriDiskCount :: Int
340− , hriSetCount :: Int
341− , hriObjectSize :: Int
342− , hriBefore :: [DriveInfo]
343− , hriAfter :: [DriveInfo]
344− } deriving (Eq, Show)
372+ { hriResultIdx :: Int,
373+ hriType :: HealItemType,
374+ hriBucket :: Bucket,
375+ hriObject :: Object,
376+ hriDetail :: Text,
377+ hriParityBlocks :: Maybe Int,
378+ hriDataBlocks :: Maybe Int,
379+ hriDiskCount :: Int,
380+ hriSetCount :: Int,
381+ hriObjectSize :: Int,
382+ hriBefore :: [DriveInfo],
383+ hriAfter :: [DriveInfo]
384+ }
385+ deriving (Eq, Show)
345386
346387instance FromJSON HealResultItem where
347− parseJSON = withObject "HealResultItem" $ \v -> HealResultItem
348− <$> v .: "resultId"
349− <*> v .: "type"
350− <*> v .: "bucket"
351− <*> v .: "object"
352− <*> v .: "detail"
353− <*> v .:? "parityBlocks"
354− <*> v .:? "dataBlocks"
355− <*> v .: "diskCount"
356− <*> v .: "setCount"
357− <*> v .: "objectSize"
358− <*> (do before <- v .: "before"
359− before .: "drives")
360− <*> (do after <- v .: "after"
361− after .: "drives")
388+ parseJSON = withObject "HealResultItem" $ \v ->
389+ HealResultItem
390+ <$> v .: "resultId"
391+ <*> v .: "type"
392+ <*> v .: "bucket"
393+ <*> v .: "object"
394+ <*> v .: "detail"
395+ <*> v .:? "parityBlocks"
396+ <*> v .:? "dataBlocks"
397+ <*> v .: "diskCount"
398+ <*> v .: "setCount"
399+ <*> v .: "objectSize"
400+ <*> ( do
401+ before <- v .: "before"
402+ before .: "drives"
403+ )
404+ <*> ( do
405+ after <- v .: "after"
406+ after .: "drives"
407+ )
362408
363409data HealStatus = HealStatus
364− { hsSummary :: Text
365− , hsStartTime :: UTCTime
366− , hsSettings :: HealOpts
367− , hsNumDisks :: Int
368− , hsFailureDetail :: Maybe Text
369− , hsItems :: Maybe [HealResultItem]
370− } deriving (Eq, Show)
410+ { hsSummary :: Text,
411+ hsStartTime :: UTCTime,
412+ hsSettings :: HealOpts,
413+ hsNumDisks :: Int,
414+ hsFailureDetail :: Maybe Text,
415+ hsItems :: Maybe [HealResultItem]
416+ }
417+ deriving (Eq, Show)
371418
372419instance FromJSON HealStatus where
373− parseJSON = withObject "HealStatus" $ \v -> HealStatus
374− <$> v .: "Summary"
375− <*> v .: "StartTime"
376− <*> v .: "Settings"
377− <*> v .: "NumDisks"
378− <*> v .:? "Detail"
379− <*> v .: "Items"
420+ parseJSON = withObject "HealStatus" $ \v ->
421+ HealStatus
422+ <$> v .: "Summary"
423+ <*> v .: "StartTime"
424+ <*> v .: "Settings"
425+ <*> v .: "NumDisks"
426+ <*> v .:? "Detail"
427+ <*> v .: "Items"
380428
381429healPath :: Maybe Bucket -> Maybe Text -> ByteString
382430healPath bucket prefix = do
383431 if (isJust bucket)
384− then encodeUtf8 $ "v1/heal/" <> fromMaybe "" bucket <> "/"
385− <> fromMaybe "" prefix
432+ then
433+ encodeUtf8 $
434+ "v1/heal/" <> fromMaybe "" bucket <> "/"
435+ <> fromMaybe "" prefix
386436 else encodeUtf8 $ "v1/heal/"
387437
388438-- | Get server version and uptime.
389439serviceStatus :: Minio ServiceStatus
390440serviceStatus = do
391− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
392− , ariPayload = PayloadBS B.empty
393− , ariPayloadHash = Nothing
394− , ariPath = "v1/service"
395− , ariHeaders = []
396− , ariQueryParams = []
397− }
398−
399− let rspBS = NC.responseBody rsp
400− case eitherDecode rspBS of
401− Right ss -> return ss
402− Left err -> throwIO $ MErrVJsonParse $ T.pack err
441+ rsp <-
442+ executeAdminRequest
443+ AdminReqInfo
444+ { ariMethod = HT.methodGet,
445+ ariPayload = PayloadBS B.empty,
446+ ariPayloadHash = Nothing,
447+ ariPath = "v1/service",
448+ ariHeaders = [],
449+ ariQueryParams = []
450+ }
451+
452+ let rspBS = NC.responseBody rsp
453+ case eitherDecode rspBS of
454+ Right ss -> return ss
455+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
403456
404457-- | Send service restart or stop action to MinIO server.
405458serviceSendAction :: ServiceAction -> Minio ()
406459serviceSendAction action = do
407− let payload = PayloadBS $ LBS.toStrict $ A.encode action
408− void $ executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
409− , ariPayload = payload
410− , ariPayloadHash = Nothing
411− , ariPath = "v1/service"
412− , ariHeaders = []
413− , ariQueryParams = []
414− }
460+ let payload = PayloadBS $ LBS.toStrict $ A.encode action
461+ void $
462+ executeAdminRequest
463+ AdminReqInfo
464+ { ariMethod = HT.methodPost,
465+ ariPayload = payload,
466+ ariPayloadHash = Nothing,
467+ ariPath = "v1/service",
468+ ariHeaders = [],
469+ ariQueryParams = []
470+ }
415471
416472-- | Get the current config file from server.
417473getConfig :: Minio ByteString
418474getConfig = do
419− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
420− , ariPayload = PayloadBS B.empty
421− , ariPayloadHash = Nothing
422− , ariPath = "v1/config"
423− , ariHeaders = []
424− , ariQueryParams = []
425− }
426− return $ LBS.toStrict $ NC.responseBody rsp
475+ rsp <-
476+ executeAdminRequest
477+ AdminReqInfo
478+ { ariMethod = HT.methodGet,
479+ ariPayload = PayloadBS B.empty,
480+ ariPayloadHash = Nothing,
481+ ariPath = "v1/config",
482+ ariHeaders = [],
483+ ariQueryParams = []
484+ }
485+ return $ LBS.toStrict $ NC.responseBody rsp
427486
428487-- | Set a new config to the server.
429488setConfig :: ByteString -> Minio SetConfigResult
430489setConfig config = do
431− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPut
432− , ariPayload = PayloadBS config
433− , ariPayloadHash = Nothing
434− , ariPath = "v1/config"
435− , ariHeaders = []
436− , ariQueryParams = []
437− }
438−
439− let rspBS = NC.responseBody rsp
440− case eitherDecode rspBS of
441− Right scr -> return scr
442− Left err -> throwIO $ MErrVJsonParse $ T.pack err
490+ rsp <-
491+ executeAdminRequest
492+ AdminReqInfo
493+ { ariMethod = HT.methodPut,
494+ ariPayload = PayloadBS config,
495+ ariPayloadHash = Nothing,
496+ ariPath = "v1/config",
497+ ariHeaders = [],
498+ ariQueryParams = []
499+ }
500+
501+ let rspBS = NC.responseBody rsp
502+ case eitherDecode rspBS of
503+ Right scr -> return scr
504+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
443505
444506-- | Get the progress of currently running heal task, this API should be
445507-- invoked right after `startHeal`. `token` is obtained after `startHeal`
446508-- which should be used to get the heal status.
447509getHealStatus :: Maybe Bucket -> Maybe Text -> Text -> Minio HealStatus
448510getHealStatus bucket prefix token = do
449− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
450− let qparams = HT.queryTextToQuery [("clientToken", Just token)]
451− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
452− , ariPayload = PayloadBS B.empty
453− , ariPayloadHash = Nothing
454− , ariPath = healPath bucket prefix
455− , ariHeaders = []
456− , ariQueryParams = qparams
457− }
458− let rspBS = NC.responseBody rsp
459− case eitherDecode rspBS of
460− Right hs -> return hs
461− Left err -> throwIO $ MErrVJsonParse $ T.pack err
511+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
512+ let qparams = HT.queryTextToQuery [("clientToken", Just token)]
513+ rsp <-
514+ executeAdminRequest
515+ AdminReqInfo
516+ { ariMethod = HT.methodPost,
517+ ariPayload = PayloadBS B.empty,
518+ ariPayloadHash = Nothing,
519+ ariPath = healPath bucket prefix,
520+ ariHeaders = [],
521+ ariQueryParams = qparams
522+ }
523+ let rspBS = NC.responseBody rsp
524+ case eitherDecode rspBS of
525+ Right hs -> return hs
526+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
462527
463528doHeal :: Maybe Bucket -> Maybe Text -> HealOpts -> Bool -> Minio HealStartResp
464529doHeal bucket prefix opts forceStart = do
465− when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
466− let payload = PayloadBS $ LBS.toStrict $ A.encode opts
467− let qparams = bool [] (HT.queryTextToQuery [("forceStart", Just "true")])
468− forceStart
469−
470− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodPost
471− , ariPayload = payload
472− , ariPayloadHash = Nothing
473− , ariPath = healPath bucket prefix
474− , ariHeaders = []
475− , ariQueryParams = qparams
476− }
477−
478− let rspBS = NC.responseBody rsp
479− case eitherDecode rspBS of
480− Right hsr -> return hsr
481− Left err -> throwIO $ MErrVJsonParse $ T.pack err
530+ when (isNothing bucket && isJust prefix) $ throwIO MErrVInvalidHealPath
531+ let payload = PayloadBS $ LBS.toStrict $ A.encode opts
532+ let qparams =
533+ bool
534+ []
535+ (HT.queryTextToQuery [("forceStart", Just "true")])
536+ forceStart
537+
538+ rsp <-
539+ executeAdminRequest
540+ AdminReqInfo
541+ { ariMethod = HT.methodPost,
542+ ariPayload = payload,
543+ ariPayloadHash = Nothing,
544+ ariPath = healPath bucket prefix,
545+ ariHeaders = [],
546+ ariQueryParams = qparams
547+ }
548+
549+ let rspBS = NC.responseBody rsp
550+ case eitherDecode rspBS of
551+ Right hsr -> return hsr
552+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
482553
483554-- | Start a heal sequence that scans data under given (possible empty)
484555-- `bucket` and `prefix`. The `recursive` bool turns on recursive
@@ -500,60 +571,75 @@ forceStartHeal bucket prefix opts = doHeal bucket prefix opts True
500571-- properties, storage information, network statistics, etc.
501572getServerInfo :: Minio [ServerInfo]
502573getServerInfo = do
503− rsp <- executeAdminRequest AdminReqInfo { ariMethod = HT.methodGet
504− , ariPayload = PayloadBS B.empty
505− , ariPayloadHash = Nothing
506− , ariPath = "v1/info"
507− , ariHeaders = []
508− , ariQueryParams = []
509− }
510− let rspBS = NC.responseBody rsp
511− case eitherDecode rspBS of
512− Right si -> return si
513− Left err -> throwIO $ MErrVJsonParse $ T.pack err
574+ rsp <-
575+ executeAdminRequest
576+ AdminReqInfo
577+ { ariMethod = HT.methodGet,
578+ ariPayload = PayloadBS B.empty,
579+ ariPayloadHash = Nothing,
580+ ariPath = "v1/info",
581+ ariHeaders = [],
582+ ariQueryParams = []
583+ }
584+ let rspBS = NC.responseBody rsp
585+ case eitherDecode rspBS of
586+ Right si -> return si
587+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
514588
515589executeAdminRequest :: AdminReqInfo -> Minio (Response LByteString)
516590executeAdminRequest ari = do
517− req <- buildAdminRequest ari
518− mgr <- asks mcConnManager
519− httpLbs req mgr
591+ req <- buildAdminRequest ari
592+ mgr <- asks mcConnManager
593+ httpLbs req mgr
520594
521595buildAdminRequest :: AdminReqInfo -> Minio NC.Request
522596buildAdminRequest areq = do
523− ci <- asks mcConnInfo
524− sha256Hash <- if | connectIsSecure ci ->
525− -- if secure connection
526− return "UNSIGNED-PAYLOAD"
527−
528− -- otherwise compute sha256
529− | otherwise -> getPayloadSHA256Hash (ariPayload areq)
530−
531− timeStamp <- liftIO getCurrentTime
532−
533− let hostHeader = (hHost, getHostAddr ci)
534− newAreq = areq { ariPayloadHash = Just sha256Hash
535− , ariHeaders = hostHeader
536− : sha256Header sha256Hash
537− : ariHeaders areq
538− }
539− signReq = toRequest ci newAreq
540− sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
541− timeStamp Nothing Nothing (ariPayloadHash newAreq)
542− signHeaders = signV4 sp signReq
543−
544− -- Update signReq with Authorization header containing v4 signature
545− return signReq {
546− NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
547− }
597+ ci <- asks mcConnInfo
598+ sha256Hash <-
599+ if
600+ | connectIsSecure ci ->
601+ -- if secure connection
602+ return "UNSIGNED-PAYLOAD"
603+ -- otherwise compute sha256
604+ | otherwise -> getPayloadSHA256Hash (ariPayload areq)
605+
606+ timeStamp <- liftIO getCurrentTime
607+
608+ let hostHeader = (hHost, getHostAddr ci)
609+ newAreq =
610+ areq
611+ { ariPayloadHash = Just sha256Hash,
612+ ariHeaders =
613+ hostHeader
614+ : sha256Header sha256Hash
615+ : ariHeaders areq
616+ }
617+ signReq = toRequest ci newAreq
618+ sp =
619+ SignParams
620+ (connectAccessKey ci)
621+ (connectSecretKey ci)
622+ timeStamp
623+ Nothing
624+ Nothing
625+ (ariPayloadHash newAreq)
626+ signHeaders = signV4 sp signReq
627+
628+ -- Update signReq with Authorization header containing v4 signature
629+ return
630+ signReq
631+ { NC.requestHeaders = ariHeaders newAreq ++ mkHeaderFromPairs signHeaders
632+ }
548633 where
549634 toRequest :: ConnectInfo -> AdminReqInfo -> NC.Request
550− toRequest ci aReq = NC.defaultRequest
551− { NC.method = ariMethod aReq
552− , NC.secure = connectIsSecure ci
553− , NC.host = encodeUtf8 $ connectHost ci
554− , NC.port = connectPort ci
555− , NC.path = B.intercalate "/" [adminPath, ariPath aReq]
556− , NC.requestHeaders = ariHeaders aReq
557− , NC.queryString = HT.renderQuery False $ ariQueryParams aReq
558− , NC.requestBody = getRequestBody (ariPayload aReq)
635+ toRequest ci aReq =
636+ NC.defaultRequest
637+ { NC.method = ariMethod aReq,
638+ NC.secure = connectIsSecure ci,
639+ NC.host = encodeUtf8 $ connectHost ci,
640+ NC.port = connectPort ci,
641+ NC.path = B.intercalate "/" [adminPath, ariPath aReq],
642+ NC.requestHeaders = ariHeaders aReq,
643+ NC.queryString = HT.renderQuery False $ ariQueryParams aReq,
644+ NC.requestBody = getRequestBody (ariPayload aReq)
559645 }
src/Network/Minio/CopyObject.hs+49−33
@@ -16,19 +16,19 @@
1616
1717module Network.Minio.CopyObject where
1818
19−import qualified Data.List as List
20−
21−import Lib.Prelude
22−
23−import Network.Minio.Data
24−import Network.Minio.Errors
25−import Network.Minio.S3API
26−import Network.Minio.Utils
27−
19+import qualified Data.List as List
20+import Lib.Prelude
21+import Network.Minio.Data
22+import Network.Minio.Errors
23+import Network.Minio.S3API
24+import Network.Minio.Utils
2825
2926-- | Copy an object using single or multipart copy strategy.
30−copyObjectInternal :: Bucket -> Object -> SourceInfo
31− -> Minio ETag
27+copyObjectInternal ::
28+ Bucket ->
29+ Object ->
30+ SourceInfo ->
31+ Minio ETag
3232copyObjectInternal b' o srcInfo = do
3333 let sBucket = srcBucket srcInfo
3434 sObject = srcObject srcInfo
@@ -43,27 +43,35 @@ copyObjectInternal b' o srcInfo = do
4343 startOffset = fst range
4444 endOffset = snd range
4545
46− when (isJust rangeMay &&
47− or [startOffset < 0, endOffset < startOffset,
48− endOffset >= fromIntegral srcSize]) $
49− throwIO $ MErrVInvalidSrcObjByteRange range
46+ when
47+ ( isJust rangeMay
48+ && or
49+ [ startOffset < 0,
50+ endOffset < startOffset,
51+ endOffset >= fromIntegral srcSize
52+ ]
53+ )
54+ $ throwIO
55+ $ MErrVInvalidSrcObjByteRange range
5056
5157 -- 1. If sz > 64MiB (minPartSize) use multipart copy, OR
5258 -- 2. If startOffset /= 0 use multipart copy
53− let destSize = (\(a, b) -> b - a + 1 ) $
54− maybe (0, srcSize - 1) identity rangeMay
59+ let destSize =
60+ (\(a, b) -> b - a + 1) $
61+ maybe (0, srcSize - 1) identity rangeMay
5562
5663 if destSize > minPartSize || (endOffset - startOffset + 1 /= srcSize)
5764 then multiPartCopyObject b' o srcInfo srcSize
58−
59− else fst <$> copyObjectSingle b' o srcInfo{srcRange = Nothing} []
65+ else fst <$> copyObjectSingle b' o srcInfo {srcRange = Nothing} []
6066
6167-- | Given the input byte range of the source object, compute the
6268-- splits for a multipart copy object procedure. Minimum part size
6369-- used is minPartSize.
6470selectCopyRanges :: (Int64, Int64) -> [(PartNumber, (Int64, Int64))]
65−selectCopyRanges (st, end) = zip pns $
66− map (\(x, y) -> (st + x, st + x + y - 1)) $ zip startOffsets partSizes
71+selectCopyRanges (st, end) =
72+ zip pns
73+ $ map (\(x, y) -> (st + x, st + x + y - 1))
74+ $ zip startOffsets partSizes
6775 where
6876 size = end - st + 1
6977 (pns, startOffsets, partSizes) = List.unzip3 $ selectPartSizes size
@@ -71,22 +79,30 @@ selectCopyRanges (st, end) = zip pns $
7179-- | Perform a multipart copy object action. Since we cannot verify
7280-- existing parts based on the source object, there is no resuming
7381-- copy action support.
74−multiPartCopyObject :: Bucket -> Object -> SourceInfo -> Int64
75− -> Minio ETag
82+multiPartCopyObject ::
83+ Bucket ->
84+ Object ->
85+ SourceInfo ->
86+ Int64 ->
87+ Minio ETag
7688multiPartCopyObject b o cps srcSize = do
7789 uid <- newMultipartUpload b o []
7890
7991 let byteRange = maybe (0, fromIntegral $ srcSize - 1) identity $ srcRange cps
8092 partRanges = selectCopyRanges byteRange
81− partSources = map (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end) }))
82− partRanges
83− dstInfo = defaultDestinationInfo { dstBucket = b, dstObject = o}
84−
85− copiedParts <- limitedMapConcurrently 10
86− (\(pn, cps') -> do
87− (etag, _) <- copyObjectPart dstInfo cps' uid pn []
88− return (pn, etag)
89− )
90− partSources
93+ partSources =
94+ map
95+ (\(x, (start, end)) -> (x, cps {srcRange = Just (start, end)}))
96+ partRanges
97+ dstInfo = defaultDestinationInfo {dstBucket = b, dstObject = o}
98+
99+ copiedParts <-
100+ limitedMapConcurrently
101+ 10
102+ ( \(pn, cps') -> do
103+ (etag, _) <- copyObjectPart dstInfo cps' uid pn []
104+ return (pn, etag)
105+ )
106+ partSources
91107
92108 completeMultipartUpload b o uid copiedParts
src/Network/Minio/Data.hs+581−490
@@ -13,45 +13,48 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−
17−{-# LANGUAGE CPP #-}
18−{-# LANGUAGE GADTs #-}
16+{-# LANGUAGE CPP #-}
17+{-# LANGUAGE GADTs #-}
1918{-# LANGUAGE GeneralizedNewtypeDeriving #-}
20−{-# LANGUAGE TypeFamilies #-}
19+{-# LANGUAGE TypeFamilies #-}
20+
2121module Network.Minio.Data where
2222
23−import qualified Conduit as C
24−import qualified Control.Concurrent.MVar as M
25−import Control.Monad.IO.Unlift (UnliftIO (..), askUnliftIO,
26− withUnliftIO)
27−import Control.Monad.Trans.Resource
28−import qualified Data.Aeson as A
29−import qualified Data.ByteArray as BA
30−import qualified Data.ByteString as B
31−import Data.CaseInsensitive (mk)
32−import qualified Data.HashMap.Strict as H
33−import qualified Data.Ini as Ini
34−import Data.String (IsString (..))
35−import qualified Data.Text as T
36−import qualified Data.Text.Encoding as TE
37−import Data.Time (defaultTimeLocale, formatTime)
38−import GHC.Show (Show (show))
39−import qualified Network.Connection as Conn
40−import Network.HTTP.Client (defaultManagerSettings)
41−import qualified Network.HTTP.Client.TLS as TLS
42−import qualified Network.HTTP.Conduit as NC
43−import Network.HTTP.Types (ByteRange, Header, Method, Query,
44− hRange)
45−import qualified Network.HTTP.Types as HT
46−import Network.Minio.Errors
47−import System.Directory (doesFileExist, getHomeDirectory)
48−import qualified System.Environment as Env
49−import System.FilePath.Posix (combine)
50−import Text.XML
51−import qualified UnliftIO as U
52−
53−import Lib.Prelude
54−import Network.Minio.Data.Crypto
23+import qualified Conduit as C
24+import qualified Control.Concurrent.MVar as M
25+import Control.Monad.Trans.Resource
26+import qualified Data.Aeson as A
27+import qualified Data.ByteArray as BA
28+import qualified Data.ByteString as B
29+import qualified Data.ByteString.Lazy as LB
30+import Data.CaseInsensitive (mk)
31+import qualified Data.HashMap.Strict as H
32+import qualified Data.Ini as Ini
33+import Data.String (IsString (..))
34+import qualified Data.Text as T
35+import qualified Data.Text.Encoding as TE
36+import Data.Time (defaultTimeLocale, formatTime)
37+import GHC.Show (Show (show))
38+import Lib.Prelude
39+import qualified Network.Connection as Conn
40+import Network.HTTP.Client (defaultManagerSettings)
41+import qualified Network.HTTP.Client.TLS as TLS
42+import qualified Network.HTTP.Conduit as NC
43+import Network.HTTP.Types
44+ ( ByteRange,
45+ Header,
46+ Method,
47+ Query,
48+ hRange,
49+ )
50+import qualified Network.HTTP.Types as HT
51+import Network.Minio.Data.Crypto
52+import Network.Minio.Errors
53+import System.Directory (doesFileExist, getHomeDirectory)
54+import qualified System.Environment as Env
55+import System.FilePath.Posix (combine)
56+import Text.XML
57+import qualified UnliftIO as U
5558
5659-- | max obj size is 5TiB
5760maxObjectSize :: Int64
@@ -71,59 +74,64 @@ maxMultipartParts = 10000
7174-- TODO: Add a type which provides typed constants for region. this
7275-- type should have a IsString instance to infer the appropriate
7376-- constant.
77+
7478-- | awsRegionMap - library constant
7579awsRegionMap :: H.HashMap Text Text
76−awsRegionMap = H.fromList [
77− ("us-east-1", "s3.amazonaws.com")
78− , ("us-east-2", "s3-us-east-2.amazonaws.com")
79− , ("us-west-1", "s3-us-west-1.amazonaws.com")
80− , ("us-west-2", "s3-us-west-2.amazonaws.com")
81− , ("ca-central-1", "s3-ca-central-1.amazonaws.com")
82− , ("ap-south-1", "s3-ap-south-1.amazonaws.com")
83− , ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
84− , ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
85− , ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
86− , ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
87− , ("eu-west-1", "s3-eu-west-1.amazonaws.com")
88− , ("eu-west-2", "s3-eu-west-2.amazonaws.com")
89− , ("eu-central-1", "s3-eu-central-1.amazonaws.com")
90− , ("sa-east-1", "s3-sa-east-1.amazonaws.com")
91− ]
80+awsRegionMap =
81+ H.fromList
82+ [ ("us-east-1", "s3.amazonaws.com"),
83+ ("us-east-2", "s3-us-east-2.amazonaws.com"),
84+ ("us-west-1", "s3-us-west-1.amazonaws.com"),
85+ ("us-west-2", "s3-us-west-2.amazonaws.com"),
86+ ("ca-central-1", "s3-ca-central-1.amazonaws.com"),
87+ ("ap-south-1", "s3-ap-south-1.amazonaws.com"),
88+ ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com"),
89+ ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com"),
90+ ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com"),
91+ ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com"),
92+ ("eu-west-1", "s3-eu-west-1.amazonaws.com"),
93+ ("eu-west-2", "s3-eu-west-2.amazonaws.com"),
94+ ("eu-central-1", "s3-eu-central-1.amazonaws.com"),
95+ ("sa-east-1", "s3-sa-east-1.amazonaws.com")
96+ ]
9297
9398-- | Connection Info data type. To create a 'ConnectInfo' value,
9499-- enable the @OverloadedStrings@ language extension and use the
95100-- `IsString` instance to provide a URL, for example:
96101--
97102-- > let c :: ConnectInfo = "https://play.min.io"
98−data ConnectInfo =
99− ConnectInfo { connectHost :: Text
100− , connectPort :: Int
101− , connectAccessKey :: Text
102− , connectSecretKey :: Text
103− , connectIsSecure :: Bool
104− , connectRegion :: Region
105− , connectAutoDiscoverRegion :: Bool
106− , connectDisableTLSCertValidation :: Bool
107− } deriving (Eq, Show)
103+data ConnectInfo = ConnectInfo
104+ { connectHost :: Text,
105+ connectPort :: Int,
106+ connectAccessKey :: Text,
107+ connectSecretKey :: Text,
108+ connectIsSecure :: Bool,
109+ connectRegion :: Region,
110+ connectAutoDiscoverRegion :: Bool,
111+ connectDisableTLSCertValidation :: Bool
112+ }
113+ deriving (Eq, Show)
108114
109115instance IsString ConnectInfo where
110− fromString str =
111− let req = NC.parseRequest_ str
112− in ConnectInfo
113− { connectHost = TE.decodeUtf8 $ NC.host req
114− , connectPort = NC.port req
115− , connectAccessKey = ""
116− , connectSecretKey = ""
117− , connectIsSecure = NC.secure req
118− , connectRegion = ""
119− , connectAutoDiscoverRegion = True
120− , connectDisableTLSCertValidation = False
121− }
116+ fromString str =
117+ let req = NC.parseRequest_ str
118+ in ConnectInfo
119+ { connectHost = TE.decodeUtf8 $ NC.host req,
120+ connectPort = NC.port req,
121+ connectAccessKey = "",
122+ connectSecretKey = "",
123+ connectIsSecure = NC.secure req,
124+ connectRegion = "",
125+ connectAutoDiscoverRegion = True,
126+ connectDisableTLSCertValidation = False
127+ }
122128
123129-- | Contains access key and secret key to access object storage.
124−data Credentials = Credentials { cAccessKey :: Text
125− , cSecretKey :: Text
126− } deriving (Eq, Show)
130+data Credentials = Credentials
131+ { cAccessKey :: Text,
132+ cSecretKey :: Text
133+ }
134+ deriving (Eq, Show)
127135
128136-- | A Provider is an action that may return Credentials. Providers
129137-- may be chained together using 'findFirst'.
@@ -133,63 +141,70 @@ type Provider = IO (Maybe Credentials)
133141-- order until Credentials are found.
134142findFirst :: [Provider] -> Provider
135143findFirst [] = return Nothing
136−findFirst (f:fs) = do c <- f
137− maybe (findFirst fs) (return . Just) c
144+findFirst (f : fs) = do
145+ c <- f
146+ maybe (findFirst fs) (return . Just) c
138147
139148-- | This Provider loads `Credentials` from @~\/.aws\/credentials@
140149fromAWSConfigFile :: Provider
141150fromAWSConfigFile = do
142− credsE <- runExceptT $ do
143− homeDir <- lift $ getHomeDirectory
144− let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
145− fileExists <- lift $ doesFileExist awsCredsFile
146− bool (throwE "FileNotFound") (return ()) fileExists
147− ini <- ExceptT $ Ini.readIniFile awsCredsFile
148− akey <- ExceptT $ return
149− $ Ini.lookupValue "default" "aws_access_key_id" ini
150− skey <- ExceptT $ return
151− $ Ini.lookupValue "default" "aws_secret_access_key" ini
152− return $ Credentials akey skey
153− return $ hush credsE
151+ credsE <- runExceptT $ do
152+ homeDir <- lift $ getHomeDirectory
153+ let awsCredsFile = homeDir `combine` ".aws" `combine` "credentials"
154+ fileExists <- lift $ doesFileExist awsCredsFile
155+ bool (throwE "FileNotFound") (return ()) fileExists
156+ ini <- ExceptT $ Ini.readIniFile awsCredsFile
157+ akey <-
158+ ExceptT $ return $
159+ Ini.lookupValue "default" "aws_access_key_id" ini
160+ skey <-
161+ ExceptT $ return $
162+ Ini.lookupValue "default" "aws_secret_access_key" ini
163+ return $ Credentials akey skey
164+ return $ hush credsE
154165
155166-- | This Provider loads `Credentials` from @AWS_ACCESS_KEY_ID@ and
156167-- @AWS_SECRET_ACCESS_KEY@ environment variables.
157168fromAWSEnv :: Provider
158169fromAWSEnv = runMaybeT $ do
159− akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
160− skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
161− return $ Credentials (T.pack akey) (T.pack skey)
170+ akey <- MaybeT $ Env.lookupEnv "AWS_ACCESS_KEY_ID"
171+ skey <- MaybeT $ Env.lookupEnv "AWS_SECRET_ACCESS_KEY"
172+ return $ Credentials (T.pack akey) (T.pack skey)
162173
163174-- | This Provider loads `Credentials` from @MINIO_ACCESS_KEY@ and
164175-- @MINIO_SECRET_KEY@ environment variables.
165176fromMinioEnv :: Provider
166177fromMinioEnv = runMaybeT $ do
167− akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
168− skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
169− return $ Credentials (T.pack akey) (T.pack skey)
178+ akey <- MaybeT $ Env.lookupEnv "MINIO_ACCESS_KEY"
179+ skey <- MaybeT $ Env.lookupEnv "MINIO_SECRET_KEY"
180+ return $ Credentials (T.pack akey) (T.pack skey)
170181
171182-- | setCredsFrom retrieves access credentials from the first
172183-- `Provider` form the given list that succeeds and sets it in the
173184-- `ConnectInfo`.
174185setCredsFrom :: [Provider] -> ConnectInfo -> IO ConnectInfo
175−setCredsFrom ps ci = do pMay <- findFirst ps
176− maybe
177− (throwIO MErrVMissingCredentials)
178− (return . (flip setCreds ci))
179− pMay
186+setCredsFrom ps ci = do
187+ pMay <- findFirst ps
188+ maybe
189+ (throwIO MErrVMissingCredentials)
190+ (return . (flip setCreds ci))
191+ pMay
180192
181193-- | setCreds sets the given `Credentials` in the `ConnectInfo`.
182194setCreds :: Credentials -> ConnectInfo -> ConnectInfo
183195setCreds (Credentials accessKey secretKey) connInfo =
184− connInfo { connectAccessKey = accessKey
185− , connectSecretKey = secretKey
186− }
196+ connInfo
197+ { connectAccessKey = accessKey,
198+ connectSecretKey = secretKey
199+ }
187200
188201-- | Set the S3 region parameter in the `ConnectInfo`
189202setRegion :: Region -> ConnectInfo -> ConnectInfo
190−setRegion r connInfo = connInfo { connectRegion = r
191− , connectAutoDiscoverRegion = False
192− }
203+setRegion r connInfo =
204+ connInfo
205+ { connectRegion = r,
206+ connectAutoDiscoverRegion = False
207+ }
193208
194209-- | Check if the connection to object storage server is secure
195210-- (i.e. uses TLS)
@@ -201,17 +216,19 @@ isConnectInfoSecure = connectIsSecure
201216-- certificates. Note that this option has no effect, if you provide
202217-- your own Manager in `mkMinioConn`.
203218disableTLSCertValidation :: ConnectInfo -> ConnectInfo
204−disableTLSCertValidation c = c { connectDisableTLSCertValidation = True }
219+disableTLSCertValidation c = c {connectDisableTLSCertValidation = True}
205220
206221getHostAddr :: ConnectInfo -> ByteString
207−getHostAddr ci = if | port == 80 || port == 443 -> toS host
208− | otherwise -> toS $
209− T.concat [ host, ":" , Lib.Prelude.show port]
222+getHostAddr ci =
223+ if
224+ | port == 80 || port == 443 -> toUtf8 host
225+ | otherwise ->
226+ toUtf8 $
227+ T.concat [host, ":", Lib.Prelude.show port]
210228 where
211229 port = connectPort ci
212230 host = connectHost ci
213231
214−
215232-- | Default Google Compute Storage ConnectInfo. Works only for
216233-- "Simple Migration" use-case with interoperability mode enabled on
217234-- GCP console. For more information -
@@ -219,23 +236,25 @@ getHostAddr ci = if | port == 80 || port == 443 -> toS host
219236--
220237-- Credentials should be supplied before use.
221238gcsCI :: ConnectInfo
222−gcsCI = setRegion "us"
223− "https://storage.googleapis.com"
224−
239+gcsCI =
240+ setRegion
241+ "us"
242+ "https://storage.googleapis.com"
225243
226244-- | Default AWS S3 ConnectInfo. Connects to "us-east-1". Credentials
227245-- should be supplied before use.
228246awsCI :: ConnectInfo
229247awsCI = "https://s3.amazonaws.com"
230248
231−
232249-- | <https://play.min.io MinIO Play Server>
233250-- ConnectInfo. Credentials are already filled in.
234251minioPlayCI :: ConnectInfo
235−minioPlayCI = let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
236− in setCreds playCreds
237− $ setRegion "us-east-1"
238− "https://play.min.io"
252+minioPlayCI =
253+ let playCreds = Credentials "Q3AM3UQ867SPQQA43P2F" "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
254+ in setCreds playCreds $
255+ setRegion
256+ "us-east-1"
257+ "https://play.min.io"
239258
240259-- |
241260-- Represents a bucket in the object store
@@ -254,80 +273,79 @@ type ETag = Text
254273-- | Data type to represent an object encryption key. Create one using
255274-- the `mkSSECKey` function.
256275newtype SSECKey = SSECKey BA.ScrubbedBytes
257− deriving (Eq, Show)
276+ deriving (Eq, Show)
258277
259278-- | Validates that the given ByteString is 32 bytes long and creates
260279-- an encryption key.
261280mkSSECKey :: MonadThrow m => ByteString -> m SSECKey
262−mkSSECKey keyBytes | B.length keyBytes /= 32 =
263− throwM MErrVInvalidEncryptionKeyLength
264− | otherwise =
265− return $ SSECKey $ BA.convert keyBytes
281+mkSSECKey keyBytes
282+ | B.length keyBytes /= 32 =
283+ throwM MErrVInvalidEncryptionKeyLength
284+ | otherwise =
285+ return $ SSECKey $ BA.convert keyBytes
266286
267287-- | Data type to represent Server-Side-Encryption settings
268288data SSE where
269− -- | Specifies SSE S3 encryption - server manages encryption keys
270− SSE :: SSE
271− -- | Specifies that KMS service should be used. The first argument
272− -- to the constructor is the Key Id to be used by the server (if
273− -- not specified, the default KMS key id is used). The second
274− -- argument is the optional KMS context that must have a
275− -- `A.ToJSON` instance - please refer to the AWS S3 documentation
276− -- for detailed information.
277− SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE
278− -- | Specifies server-side encryption with customer provided
279− -- key. The argument is the encryption key to be used.
280− SSEC :: SSECKey -> SSE
289+ -- | Specifies SSE S3 encryption - server manages encryption keys
290+ SSE :: SSE
291+ -- | Specifies that KMS service should be used. The first argument
292+ -- to the constructor is the Key Id to be used by the server (if
293+ -- not specified, the default KMS key id is used). The second
294+ -- argument is the optional KMS context that must have a
295+ -- `A.ToJSON` instance - please refer to the AWS S3 documentation
296+ -- for detailed information.
297+ SSEKMS :: A.ToJSON a => Maybe ByteString -> Maybe a -> SSE
298+ -- | Specifies server-side encryption with customer provided
299+ -- key. The argument is the encryption key to be used.
300+ SSEC :: SSECKey -> SSE
281301
282302toPutObjectHeaders :: SSE -> [HT.Header]
283303toPutObjectHeaders sseArg =
284− let sseHeader = "x-amz-server-side-encryption"
285− sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
286− sseKmsContextHeader = sseHeader <> "-context"
287− ssecAlgo = sseHeader <> "-customer-algorithm"
288− ssecKey = sseHeader <> "-customer-key"
289− ssecKeyMD5 = ssecKey <> "-MD5"
290−
291− in case sseArg of
292− SSE -> [(sseHeader, "AES256")]
293−
294− SSEKMS keyIdMay ctxMay ->
295− [(sseHeader, "aws:kms")] ++
296− maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay ++
297− maybe [] (\k -> [(sseKmsContextHeader, toS $ A.encode k)]) ctxMay
298−
299− SSEC (SSECKey sb) ->
300− [(ssecAlgo, "AES256"),
301− (ssecKey, encodeToBase64 sb),
302− (ssecKeyMD5, hashMD5ToBase64 sb)]
304+ let sseHeader = "x-amz-server-side-encryption"
305+ sseKmsIdHeader = sseHeader <> "-aws-kms-key-id"
306+ sseKmsContextHeader = sseHeader <> "-context"
307+ ssecAlgo = sseHeader <> "-customer-algorithm"
308+ ssecKey = sseHeader <> "-customer-key"
309+ ssecKeyMD5 = ssecKey <> "-MD5"
310+ in case sseArg of
311+ SSE -> [(sseHeader, "AES256")]
312+ SSEKMS keyIdMay ctxMay ->
313+ [(sseHeader, "aws:kms")]
314+ ++ maybe [] (\k -> [(sseKmsIdHeader, k)]) keyIdMay
315+ ++ maybe [] (\k -> [(sseKmsContextHeader, LB.toStrict $ A.encode k)]) ctxMay
316+ SSEC (SSECKey sb) ->
317+ [ (ssecAlgo, "AES256"),
318+ (ssecKey, encodeToBase64 sb),
319+ (ssecKeyMD5, hashMD5ToBase64 sb)
320+ ]
303321
304322-- | Data type for options in PutObject call. Start with the empty
305323-- `defaultPutObjectOptions` and use various the various poo*
306324-- accessors.
307−data PutObjectOptions = PutObjectOptions {
308− -- | Set a standard MIME type describing the format of the object.
309− pooContentType :: Maybe Text
310− -- | Set what content encodings have been applied to the object and thus
311− -- what decoding mechanisms must be applied to obtain the media-type
312− -- referenced by the Content-Type header field.
313− , pooContentEncoding :: Maybe Text
314− -- | Set presentational information for the object.
315− , pooContentDisposition :: Maybe Text
316− -- | Set to specify caching behavior for the object along the
317− -- request/reply chain.
318− , pooCacheControl :: Maybe Text
319− -- | Set to describe the language(s) intended for the audience.
320− , pooContentLanguage :: Maybe Text
321− -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
322− -- performance needs, storage class is @STANDARD@ by default (i.e
323− -- when Nothing is passed).
324− , pooStorageClass :: Maybe Text
325− -- | Set user defined metadata to store with the object.
326− , pooUserMetadata :: [(Text, Text)]
327− -- | Set number of worker threads used to upload an object.
328− , pooNumThreads :: Maybe Word
329− -- | Set object encryption parameters for the request.
330− , pooSSE :: Maybe SSE
325+data PutObjectOptions = PutObjectOptions
326+ { -- | Set a standard MIME type describing the format of the object.
327+ pooContentType :: Maybe Text,
328+ -- | Set what content encodings have been applied to the object and thus
329+ -- what decoding mechanisms must be applied to obtain the media-type
330+ -- referenced by the Content-Type header field.
331+ pooContentEncoding :: Maybe Text,
332+ -- | Set presentational information for the object.
333+ pooContentDisposition :: Maybe Text,
334+ -- | Set to specify caching behavior for the object along the
335+ -- request/reply chain.
336+ pooCacheControl :: Maybe Text,
337+ -- | Set to describe the language(s) intended for the audience.
338+ pooContentLanguage :: Maybe Text,
339+ -- | Set to @STANDARD@ or @REDUCED_REDUNDANCY@ depending on your
340+ -- performance needs, storage class is @STANDARD@ by default (i.e
341+ -- when Nothing is passed).
342+ pooStorageClass :: Maybe Text,
343+ -- | Set user defined metadata to store with the object.
344+ pooUserMetadata :: [(Text, Text)],
345+ -- | Set number of worker threads used to upload an object.
346+ pooNumThreads :: Maybe Word,
347+ -- | Set object encryption parameters for the request.
348+ pooSSE :: Maybe SSE
331349 }
332350
333351-- | Provide default `PutObjectOptions`.
@@ -338,47 +356,55 @@ defaultPutObjectOptions = PutObjectOptions Nothing Nothing Nothing Nothing Nothi
338356-- stripped and a Just is returned.
339357userMetadataHeaderNameMaybe :: Text -> Maybe Text
340358userMetadataHeaderNameMaybe k =
341− let prefix = T.toCaseFold "X-Amz-Meta-"
342− n = T.length prefix
343− in if T.toCaseFold (T.take n k) == prefix
344− then Just (T.drop n k)
345− else Nothing
359+ let prefix = T.toCaseFold "X-Amz-Meta-"
360+ n = T.length prefix
361+ in if T.toCaseFold (T.take n k) == prefix
362+ then Just (T.drop n k)
363+ else Nothing
346364
347365addXAmzMetaPrefix :: Text -> Text
348−addXAmzMetaPrefix s | isJust (userMetadataHeaderNameMaybe s) = s
349− | otherwise = "X-Amz-Meta-" <> s
366+addXAmzMetaPrefix s
367+ | isJust (userMetadataHeaderNameMaybe s) = s
368+ | otherwise = "X-Amz-Meta-" <> s
350369
351370mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
352371mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix $ x, encodeUtf8 y))
353372
354373pooToHeaders :: PutObjectOptions -> [HT.Header]
355−pooToHeaders poo = userMetadata
356− ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
357− ++ maybe [] toPutObjectHeaders (pooSSE poo)
374+pooToHeaders poo =
375+ userMetadata
376+ ++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
377+ ++ maybe [] toPutObjectHeaders (pooSSE poo)
358378 where
359− tupToMaybe (k, Just v) = Just (k, v)
379+ tupToMaybe (k, Just v) = Just (k, v)
360380 tupToMaybe (_, Nothing) = Nothing
361−
362381 userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
363−
364− names = ["content-type",
365− "content-encoding",
366− "content-disposition",
367− "content-language",
368− "cache-control",
369− "x-amz-storage-class"]
370− values = map (fmap encodeUtf8 . (poo &))
371− [pooContentType, pooContentEncoding,
372− pooContentDisposition, pooContentLanguage,
373− pooCacheControl, pooStorageClass]
374−
382+ names =
383+ [ "content-type",
384+ "content-encoding",
385+ "content-disposition",
386+ "content-language",
387+ "cache-control",
388+ "x-amz-storage-class"
389+ ]
390+ values =
391+ map
392+ (fmap encodeUtf8 . (poo &))
393+ [ pooContentType,
394+ pooContentEncoding,
395+ pooContentDisposition,
396+ pooContentLanguage,
397+ pooCacheControl,
398+ pooStorageClass
399+ ]
375400
376401-- |
377402-- BucketInfo returned for list buckets call
378−data BucketInfo = BucketInfo {
379− biName :: Bucket
380− , biCreationDate :: UTCTime
381− } deriving (Show, Eq)
403+data BucketInfo = BucketInfo
404+ { biName :: Bucket,
405+ biCreationDate :: UTCTime
406+ }
407+ deriving (Show, Eq)
382408
383409-- | A type alias to represent a part-number for multipart upload
384410type PartNumber = Int16
@@ -391,95 +417,116 @@ type PartTuple = (PartNumber, ETag)
391417
392418-- | Represents result from a listing of object parts of an ongoing
393419-- multipart upload.
394−data ListPartsResult = ListPartsResult {
395− lprHasMore :: Bool
396− , lprNextPart :: Maybe Int
397− , lprParts :: [ObjectPartInfo]
398− } deriving (Show, Eq)
420+data ListPartsResult = ListPartsResult
421+ { lprHasMore :: Bool,
422+ lprNextPart :: Maybe Int,
423+ lprParts :: [ObjectPartInfo]
424+ }
425+ deriving (Show, Eq)
399426
400427-- | Represents information about an object part in an ongoing
401428-- multipart upload.
402−data ObjectPartInfo = ObjectPartInfo {
403− opiNumber :: PartNumber
404− , opiETag :: ETag
405− , opiSize :: Int64
406− , opiModTime :: UTCTime
407− } deriving (Show, Eq)
429+data ObjectPartInfo = ObjectPartInfo
430+ { opiNumber :: PartNumber,
431+ opiETag :: ETag,
432+ opiSize :: Int64,
433+ opiModTime :: UTCTime
434+ }
435+ deriving (Show, Eq)
408436
409437-- | Represents result from a listing of incomplete uploads to a
410438-- bucket.
411−data ListUploadsResult = ListUploadsResult {
412− lurHasMore :: Bool
413− , lurNextKey :: Maybe Text
414− , lurNextUpload :: Maybe Text
415− , lurUploads :: [(Object, UploadId, UTCTime)]
416− , lurCPrefixes :: [Text]
417− } deriving (Show, Eq)
439+data ListUploadsResult = ListUploadsResult
440+ { lurHasMore :: Bool,
441+ lurNextKey :: Maybe Text,
442+ lurNextUpload :: Maybe Text,
443+ lurUploads :: [(Object, UploadId, UTCTime)],
444+ lurCPrefixes :: [Text]
445+ }
446+ deriving (Show, Eq)
418447
419448-- | Represents information about a multipart upload.
420−data UploadInfo = UploadInfo {
421− uiKey :: Object
422− , uiUploadId :: UploadId
423− , uiInitTime :: UTCTime
424− , uiSize :: Int64
425− } deriving (Show, Eq)
449+data UploadInfo = UploadInfo
450+ { uiKey :: Object,
451+ uiUploadId :: UploadId,
452+ uiInitTime :: UTCTime,
453+ uiSize :: Int64
454+ }
455+ deriving (Show, Eq)
426456
427457-- | Represents result from a listing of objects in a bucket.
428−data ListObjectsResult = ListObjectsResult {
429− lorHasMore :: Bool
430− , lorNextToken :: Maybe Text
431− , lorObjects :: [ObjectInfo]
432− , lorCPrefixes :: [Text]
433− } deriving (Show, Eq)
458+data ListObjectsResult = ListObjectsResult
459+ { lorHasMore :: Bool,
460+ lorNextToken :: Maybe Text,
461+ lorObjects :: [ObjectInfo],
462+ lorCPrefixes :: [Text]
463+ }
464+ deriving (Show, Eq)
434465
435466-- | Represents result from a listing of objects version 1 in a bucket.
436−data ListObjectsV1Result = ListObjectsV1Result {
437− lorHasMore' :: Bool
438− , lorNextMarker :: Maybe Text
439− , lorObjects' :: [ObjectInfo]
440− , lorCPrefixes' :: [Text]
441− } deriving (Show, Eq)
467+data ListObjectsV1Result = ListObjectsV1Result
468+ { lorHasMore' :: Bool,
469+ lorNextMarker :: Maybe Text,
470+ lorObjects' :: [ObjectInfo],
471+ lorCPrefixes' :: [Text]
472+ }
473+ deriving (Show, Eq)
442474
443475-- | Represents information about an object.
444476data ObjectInfo = ObjectInfo
445− { oiObject :: Object -- ^ Object key
446− , oiModTime :: UTCTime -- ^ Modification time of the object
447− , oiETag :: ETag -- ^ ETag of the object
448− , oiSize :: Int64 -- ^ Size of the object in bytes
449− , oiUserMetadata :: H.HashMap Text Text -- ^ A map of user-metadata
450− -- pairs stored with an
451− -- object (keys will not
452− -- have the @X-Amz-Meta-@
453− -- prefix).
454− , oiMetadata :: H.HashMap Text Text -- ^ A map of metadata
455− -- key-value pairs (not
456− -- including the
457− -- user-metadata pairs)
458− } deriving (Show, Eq)
477+ { -- | Object key
478+ oiObject :: Object,
479+ -- | Modification time of the object
480+ oiModTime :: UTCTime,
481+ -- | ETag of the object
482+ oiETag :: ETag,
483+ -- | Size of the object in bytes
484+ oiSize :: Int64,
485+ -- | A map of user-metadata
486+ -- pairs stored with an
487+ -- object (keys will not
488+ -- have the @X-Amz-Meta-@
489+ -- prefix).
490+ oiUserMetadata :: H.HashMap Text Text,
491+ -- | A map of metadata
492+ -- key-value pairs (not
493+ -- including the
494+ -- user-metadata pairs)
495+ oiMetadata :: H.HashMap Text Text
496+ }
497+ deriving (Show, Eq)
459498
460499-- | Represents source object in server-side copy object
461500data SourceInfo = SourceInfo
462− { srcBucket :: Text -- ^ Bucket containing the source object
463− , srcObject :: Text -- ^ Source object key
464− , srcRange :: Maybe (Int64, Int64) -- ^ Source object
465− -- byte-range
466− -- (inclusive)
467− , srcIfMatch :: Maybe Text -- ^ ETag condition on source -
468− -- object is copied only if the
469− -- source object's ETag matches
470− -- this value.
471− , srcIfNoneMatch :: Maybe Text -- ^ ETag not match condition
472− -- on source - object is copied
473− -- if ETag does not match this
474− -- value.
475− , srcIfModifiedSince :: Maybe UTCTime -- ^ Copy source object only
476− -- if the source has been
477− -- modified since this time.
478− , srcIfUnmodifiedSince :: Maybe UTCTime -- ^ Copy source object only
479− -- if the source has been
480− -- un-modified since this
481− -- given time.
482− } deriving (Show, Eq)
501+ { -- | Bucket containing the source object
502+ srcBucket :: Text,
503+ -- | Source object key
504+ srcObject :: Text,
505+ -- | Source object
506+ -- byte-range
507+ -- (inclusive)
508+ srcRange :: Maybe (Int64, Int64),
509+ -- | ETag condition on source -
510+ -- object is copied only if the
511+ -- source object's ETag matches
512+ -- this value.
513+ srcIfMatch :: Maybe Text,
514+ -- | ETag not match condition
515+ -- on source - object is copied
516+ -- if ETag does not match this
517+ -- value.
518+ srcIfNoneMatch :: Maybe Text,
519+ -- | Copy source object only
520+ -- if the source has been
521+ -- modified since this time.
522+ srcIfModifiedSince :: Maybe UTCTime,
523+ -- | Copy source object only
524+ -- if the source has been
525+ -- un-modified since this
526+ -- given time.
527+ srcIfUnmodifiedSince :: Maybe UTCTime
528+ }
529+ deriving (Show, Eq)
483530
484531-- | Provide a default for `SourceInfo`
485532defaultSourceInfo :: SourceInfo
@@ -487,9 +534,12 @@ defaultSourceInfo = SourceInfo "" "" Nothing Nothing Nothing Nothing Nothing
487534
488535-- | Represents destination object in server-side copy object
489536data DestinationInfo = DestinationInfo
490− { dstBucket :: Text -- ^ Destination bucket
491− , dstObject :: Text -- ^ Destination object key
492− } deriving (Show, Eq)
537+ { -- | Destination bucket
538+ dstBucket :: Text,
539+ -- | Destination object key
540+ dstObject :: Text
541+ }
542+ deriving (Show, Eq)
493543
494544-- | Provide a default for `DestinationInfo`
495545defaultDestinationInfo :: DestinationInfo
@@ -498,96 +548,105 @@ defaultDestinationInfo = DestinationInfo "" ""
498548-- | Data type for options when getting an object from the
499549-- service. Start with the empty `defaultGetObjectOptions` and modify
500550-- it using the goo* functions.
501−data GetObjectOptions = GetObjectOptions {
502− -- | Set object's data of given offset begin and end,
551+data GetObjectOptions = GetObjectOptions
552+ { -- | Set object's data of given offset begin and end,
503553 -- [ByteRangeFromTo 0 9] means first ten bytes of the source object.
504− gooRange :: Maybe ByteRange
554+ gooRange :: Maybe ByteRange,
505555 -- | Set matching ETag condition, GetObject which matches the following
506556 -- ETag.
507− , gooIfMatch :: Maybe ETag
557+ gooIfMatch :: Maybe ETag,
508558 -- | Set matching ETag none condition, GetObject which does not match
509559 -- the following ETag.
510− , gooIfNoneMatch :: Maybe ETag
560+ gooIfNoneMatch :: Maybe ETag,
511561 -- | Set object unmodified condition, GetObject unmodified since given time.
512− , gooIfUnmodifiedSince :: Maybe UTCTime
562+ gooIfUnmodifiedSince :: Maybe UTCTime,
513563 -- | Set object modified condition, GetObject modified since given time.
514− , gooIfModifiedSince :: Maybe UTCTime
564+ gooIfModifiedSince :: Maybe UTCTime,
515565 -- | Specify SSE-C key
516− , gooSSECKey :: Maybe SSECKey
566+ gooSSECKey :: Maybe SSECKey
517567 }
518568
519569-- | Provide default `GetObjectOptions`.
520570defaultGetObjectOptions :: GetObjectOptions
521571defaultGetObjectOptions =
522− GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
572+ GetObjectOptions Nothing Nothing Nothing Nothing Nothing Nothing
523573
524574gooToHeaders :: GetObjectOptions -> [HT.Header]
525−gooToHeaders goo = rangeHdr ++ zip names values
526− ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
575+gooToHeaders goo =
576+ rangeHdr ++ zip names values
577+ ++ maybe [] (toPutObjectHeaders . SSEC) (gooSSECKey goo)
527578 where
528− names = ["If-Match",
529− "If-None-Match",
530− "If-Unmodified-Since",
531− "If-Modified-Since"]
532− values = mapMaybe (fmap encodeUtf8 . (goo &))
533− [gooIfMatch, gooIfNoneMatch,
534− fmap formatRFC1123 . gooIfUnmodifiedSince,
535− fmap formatRFC1123 . gooIfModifiedSince]
536− rangeHdr = maybe [] (\a -> [(hRange, HT.renderByteRanges [a])])
537− $ gooRange goo
579+ names =
580+ [ "If-Match",
581+ "If-None-Match",
582+ "If-Unmodified-Since",
583+ "If-Modified-Since"
584+ ]
585+ values =
586+ mapMaybe
587+ (fmap encodeUtf8 . (goo &))
588+ [ gooIfMatch,
589+ gooIfNoneMatch,
590+ fmap formatRFC1123 . gooIfUnmodifiedSince,
591+ fmap formatRFC1123 . gooIfModifiedSince
592+ ]
593+ rangeHdr =
594+ maybe [] (\a -> [(hRange, HT.renderByteRanges [a])]) $
595+ gooRange goo
538596
539597-- | Data type returned by 'getObject' representing the object being
540598-- retrieved. Use the @gor*@ functions to access its contents.
541−data GetObjectResponse = GetObjectResponse {
542− -- | ObjectInfo of the object being retrieved.
543− gorObjectInfo :: ObjectInfo
544− -- | A conduit of the bytes of the object.
545− , gorObjectStream :: C.ConduitM () ByteString Minio ()
546− }
599+data GetObjectResponse = GetObjectResponse
600+ { -- | ObjectInfo of the object being retrieved.
601+ gorObjectInfo :: ObjectInfo,
602+ -- | A conduit of the bytes of the object.
603+ gorObjectStream :: C.ConduitM () ByteString Minio ()
604+ }
547605
548606-- | A data-type for events that can occur in the object storage
549607-- server. Reference:
550608-- https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types
551−data Event = ObjectCreated
552− | ObjectCreatedPut
553− | ObjectCreatedPost
554− | ObjectCreatedCopy
555− | ObjectCreatedMultipartUpload
556− | ObjectRemoved
557− | ObjectRemovedDelete
558− | ObjectRemovedDeleteMarkerCreated
559− | ReducedRedundancyLostObject
560− deriving (Eq)
609+data Event
610+ = ObjectCreated
611+ | ObjectCreatedPut
612+ | ObjectCreatedPost
613+ | ObjectCreatedCopy
614+ | ObjectCreatedMultipartUpload
615+ | ObjectRemoved
616+ | ObjectRemovedDelete
617+ | ObjectRemovedDeleteMarkerCreated
618+ | ReducedRedundancyLostObject
619+ deriving (Eq)
561620
562621instance Show Event where
563− show ObjectCreated = "s3:ObjectCreated:*"
564− show ObjectCreatedPut = "s3:ObjectCreated:Put"
565− show ObjectCreatedPost = "s3:ObjectCreated:Post"
566− show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
567− show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
568− show ObjectRemoved = "s3:ObjectRemoved:*"
569− show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
622+ show ObjectCreated = "s3:ObjectCreated:*"
623+ show ObjectCreatedPut = "s3:ObjectCreated:Put"
624+ show ObjectCreatedPost = "s3:ObjectCreated:Post"
625+ show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
626+ show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
627+ show ObjectRemoved = "s3:ObjectRemoved:*"
628+ show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
570629 show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
571− show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
630+ show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
572631
573632textToEvent :: Text -> Maybe Event
574633textToEvent t = case t of
575− "s3:ObjectCreated:*" -> Just ObjectCreated
576− "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
577− "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
578− "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
579− "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
580− "s3:ObjectRemoved:*" -> Just ObjectRemoved
581− "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
634+ "s3:ObjectCreated:*" -> Just ObjectCreated
635+ "s3:ObjectCreated:Put" -> Just ObjectCreatedPut
636+ "s3:ObjectCreated:Post" -> Just ObjectCreatedPost
637+ "s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
638+ "s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
639+ "s3:ObjectRemoved:*" -> Just ObjectRemoved
640+ "s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
582641 "s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
583− "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
584− _ -> Nothing
585−
642+ "s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
643+ _ -> Nothing
586644
587645-- | Filter data type - part of notification configuration
588646data Filter = Filter
589647 { fFilter :: FilterKey
590− } deriving (Show, Eq)
648+ }
649+ deriving (Show, Eq)
591650
592651-- | defaultFilter is empty, used to create a notification
593652-- configuration.
@@ -597,7 +656,8 @@ defaultFilter = Filter defaultFilterKey
597656-- | FilterKey contains FilterRules, and is part of a Filter.
598657data FilterKey = FilterKey
599658 { fkKey :: FilterRules
600− } deriving (Show, Eq)
659+ }
660+ deriving (Show, Eq)
601661
602662-- | defaultFilterKey is empty, used to create notification
603663-- configuration.
@@ -607,14 +667,14 @@ defaultFilterKey = FilterKey defaultFilterRules
607667-- | FilterRules represents a collection of `FilterRule`s.
608668data FilterRules = FilterRules
609669 { frFilterRules :: [FilterRule]
610− } deriving (Show, Eq)
670+ }
671+ deriving (Show, Eq)
611672
612673-- | defaultFilterRules is empty, used to create notification
613674-- configuration.
614675defaultFilterRules :: FilterRules
615676defaultFilterRules = FilterRules []
616677
617−
618678-- | A filter rule that can act based on the suffix or prefix of an
619679-- object. As an example, let's create two filter rules:
620680--
@@ -625,9 +685,10 @@ defaultFilterRules = FilterRules []
625685-- for objects having a suffix of ".jpg", and the @prefixRule@
626686-- restricts it to objects having a prefix of "images/".
627687data FilterRule = FilterRule
628− { frName :: Text
629− , frValue :: Text
630− } deriving (Show, Eq)
688+ { frName :: Text,
689+ frValue :: Text
690+ }
691+ deriving (Show, Eq)
631692
632693-- | Arn is an alias of Text
633694type Arn = Text
@@ -636,11 +697,12 @@ type Arn = Text
636697-- notification system. It could represent a Queue, Topic or Lambda
637698-- Function configuration.
638699data NotificationConfig = NotificationConfig
639− { ncId :: Text
640− , ncArn :: Arn
641− , ncEvents :: [Event]
642− , ncFilter :: Filter
643− } deriving (Show, Eq)
700+ { ncId :: Text,
701+ ncArn :: Arn,
702+ ncEvents :: [Event],
703+ ncFilter :: Filter
704+ }
705+ deriving (Show, Eq)
644706
645707-- | A data-type to represent bucket notification configuration. It is
646708-- a collection of queue, topic or lambda function configurations. The
@@ -648,16 +710,16 @@ data NotificationConfig = NotificationConfig
648710-- described at
649711-- <https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTnotification.html>
650712data Notification = Notification
651− { nQueueConfigurations :: [NotificationConfig]
652− , nTopicConfigurations :: [NotificationConfig]
653− , nCloudFunctionConfigurations :: [NotificationConfig]
654− } deriving (Eq, Show)
713+ { nQueueConfigurations :: [NotificationConfig],
714+ nTopicConfigurations :: [NotificationConfig],
715+ nCloudFunctionConfigurations :: [NotificationConfig]
716+ }
717+ deriving (Eq, Show)
655718
656719-- | The default notification configuration is empty.
657720defaultNotification :: Notification
658721defaultNotification = Notification [] [] []
659722
660−
661723--------------------------------------------------------------------------
662724-- Select API Related Types
663725--------------------------------------------------------------------------
@@ -665,38 +727,42 @@ defaultNotification = Notification [] [] []
665727-- | SelectRequest represents the Select API call. Use the
666728-- `selectRequest` function to create a value of this type.
667729data SelectRequest = SelectRequest
668− { srExpression :: Text
669− , srExpressionType :: ExpressionType
670− , srInputSerialization :: InputSerialization
671− , srOutputSerialization :: OutputSerialization
672− , srRequestProgressEnabled :: Maybe Bool
673− } deriving (Eq, Show)
730+ { srExpression :: Text,
731+ srExpressionType :: ExpressionType,
732+ srInputSerialization :: InputSerialization,
733+ srOutputSerialization :: OutputSerialization,
734+ srRequestProgressEnabled :: Maybe Bool
735+ }
736+ deriving (Eq, Show)
674737
675738data ExpressionType = SQL
676− deriving (Eq, Show)
739+ deriving (Eq, Show)
677740
678741-- | InputSerialization represents format information of the input
679742-- object being queried. Use one of the smart constructors such as
680743-- `defaultCsvInput` as a starting value, and add compression info
681744-- using `setInputCompressionType`
682745data InputSerialization = InputSerialization
683− { isCompressionType :: Maybe CompressionType
684− , isFormatInfo :: InputFormatInfo
685− } deriving (Eq, Show)
746+ { isCompressionType :: Maybe CompressionType,
747+ isFormatInfo :: InputFormatInfo
748+ }
749+ deriving (Eq, Show)
686750
687751-- | Data type representing the compression setting in a Select
688752-- request.
689−data CompressionType = CompressionTypeNone
690− | CompressionTypeGzip
691− | CompressionTypeBzip2
692− deriving (Eq, Show)
753+data CompressionType
754+ = CompressionTypeNone
755+ | CompressionTypeGzip
756+ | CompressionTypeBzip2
757+ deriving (Eq, Show)
693758
694759-- | Data type representing input object format information in a
695760-- Select request.
696−data InputFormatInfo = InputFormatCSV CSVInputProp
697− | InputFormatJSON JSONInputProp
698− | InputFormatParquet
699− deriving (Eq, Show)
761+data InputFormatInfo
762+ = InputFormatCSV CSVInputProp
763+ | InputFormatJSON JSONInputProp
764+ | InputFormatParquet
765+ deriving (Eq, Show)
700766
701767-- | defaultCsvInput returns InputSerialization with default CSV
702768-- format, and without any compression setting.
@@ -706,14 +772,18 @@ defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)
706772-- | linesJsonInput returns InputSerialization with JSON line based
707773-- format with no compression setting.
708774linesJsonInput :: InputSerialization
709−linesJsonInput = InputSerialization Nothing
710− (InputFormatJSON $ JSONInputProp JSONTypeLines)
775+linesJsonInput =
776+ InputSerialization
777+ Nothing
778+ (InputFormatJSON $ JSONInputProp JSONTypeLines)
711779
712780-- | documentJsonInput returns InputSerialization with JSON document
713781-- based format with no compression setting.
714782documentJsonInput :: InputSerialization
715−documentJsonInput = InputSerialization Nothing
716− (InputFormatJSON $ JSONInputProp JSONTypeDocument)
783+documentJsonInput =
784+ InputSerialization
785+ Nothing
786+ (InputFormatJSON $ JSONInputProp JSONTypeDocument)
717787
718788-- | defaultParquetInput returns InputSerialization with Parquet
719789-- format, and no compression setting.
@@ -722,12 +792,14 @@ defaultParquetInput = InputSerialization Nothing InputFormatParquet
722792
723793-- | setInputCompressionType sets the compression type for the input
724794-- of the SelectRequest
725−setInputCompressionType :: CompressionType -> SelectRequest
726− -> SelectRequest
795+setInputCompressionType ::
796+ CompressionType ->
797+ SelectRequest ->
798+ SelectRequest
727799setInputCompressionType c i =
728− let is = srInputSerialization i
729− is' = is { isCompressionType = Just c }
730− in i { srInputSerialization = is' }
800+ let is = srInputSerialization i
801+ is' = is {isCompressionType = Just c}
802+ in i {srInputSerialization = is'}
731803
732804-- | defaultCsvOutput returns OutputSerialization with default CSV
733805-- format.
@@ -744,29 +816,33 @@ defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)
744816-- SelectRequest with the SQL query text given by @query@, the input
745817-- serialization settings (compression format and format information)
746818-- @inputSer@ and the output serialization settings @outputSer@.
747−selectRequest :: Text -> InputSerialization -> OutputSerialization
748− -> SelectRequest
819+selectRequest ::
820+ Text ->
821+ InputSerialization ->
822+ OutputSerialization ->
823+ SelectRequest
749824selectRequest sqlQuery inputSer outputSer =
750− SelectRequest { srExpression = sqlQuery
751− , srExpressionType = SQL
752− , srInputSerialization = inputSer
753− , srOutputSerialization = outputSer
754− , srRequestProgressEnabled = Nothing
755− }
825+ SelectRequest
826+ { srExpression = sqlQuery,
827+ srExpressionType = SQL,
828+ srInputSerialization = inputSer,
829+ srOutputSerialization = outputSer,
830+ srRequestProgressEnabled = Nothing
831+ }
756832
757833-- | setRequestProgressEnabled sets the flag for turning on progress
758834-- messages when the Select response is being streamed back to the
759835-- client.
760836setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
761837setRequestProgressEnabled enabled sr =
762− sr { srRequestProgressEnabled = Just enabled }
838+ sr {srRequestProgressEnabled = Just enabled}
763839
764840type CSVInputProp = CSVProp
765841
766842-- | CSVProp represents CSV format properties. It is built up using
767843-- the Monoid instance.
768844data CSVProp = CSVProp (H.HashMap Text Text)
769− deriving (Eq, Show)
845+ deriving (Eq, Show)
770846
771847#if (__GLASGOW_HASKELL__ >= 804)
772848instance Semigroup CSVProp where
@@ -774,7 +850,8 @@ instance Semigroup CSVProp where
774850#endif
775851
776852instance Monoid CSVProp where
777− mempty = CSVProp mempty
853+ mempty = CSVProp mempty
854+
778855#if (__GLASGOW_HASKELL__ < 804)
779856 mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)
780857#endif
@@ -801,17 +878,20 @@ quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"
801878-- | FileHeaderInfo specifies information about column headers for CSV
802879-- format.
803880data FileHeaderInfo
804− = FileHeaderNone -- ^ No column headers are present
805− | FileHeaderUse -- ^ Headers are present and they should be used
806− | FileHeaderIgnore -- ^ Header are present, but should be ignored
807− deriving (Eq, Show)
881+ = -- | No column headers are present
882+ FileHeaderNone
883+ | -- | Headers are present and they should be used
884+ FileHeaderUse
885+ | -- | Header are present, but should be ignored
886+ FileHeaderIgnore
887+ deriving (Eq, Show)
808888
809889-- | Specify the CSV file header info property.
810890fileHeaderInfo :: FileHeaderInfo -> CSVProp
811891fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString
812892 where
813− toString FileHeaderNone = "NONE"
814− toString FileHeaderUse = "USE"
893+ toString FileHeaderNone = "NONE"
894+ toString FileHeaderUse = "USE"
815895 toString FileHeaderIgnore = "IGNORE"
816896
817897-- | Specify the CSV comment character property. Lines starting with
@@ -825,25 +905,25 @@ allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter"
825905
826906-- | Set the CSV format properties in the InputSerialization.
827907setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
828−setInputCSVProps p is = is { isFormatInfo = InputFormatCSV p }
908+setInputCSVProps p is = is {isFormatInfo = InputFormatCSV p}
829909
830910-- | Set the CSV format properties in the OutputSerialization.
831911outputCSVFromProps :: CSVProp -> OutputSerialization
832912outputCSVFromProps p = OutputSerializationCSV p
833913
834−data JSONInputProp = JSONInputProp { jsonipType :: JSONType }
835− deriving (Eq, Show)
914+data JSONInputProp = JSONInputProp {jsonipType :: JSONType}
915+ deriving (Eq, Show)
836916
837917data JSONType = JSONTypeDocument | JSONTypeLines
838− deriving (Eq, Show)
839−
918+ deriving (Eq, Show)
840919
841920-- | OutputSerialization represents output serialization settings for
842921-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
843922-- a starting point.
844−data OutputSerialization = OutputSerializationJSON JSONOutputProp
845− | OutputSerializationCSV CSVOutputProp
846− deriving (Eq, Show)
923+data OutputSerialization
924+ = OutputSerializationJSON JSONOutputProp
925+ | OutputSerializationCSV CSVOutputProp
926+ deriving (Eq, Show)
847927
848928type CSVOutputProp = CSVProp
849929
@@ -852,37 +932,40 @@ quoteFields :: QuoteFields -> CSVProp
852932quoteFields q = CSVProp $ H.singleton "QuoteFields" $
853933 case q of
854934 QuoteFieldsAsNeeded -> "ASNEEDED"
855− QuoteFieldsAlways -> "ALWAYS"
935+ QuoteFieldsAlways -> "ALWAYS"
856936
857937-- | Represent the QuoteField setting.
858938data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
859− deriving (Eq, Show)
939+ deriving (Eq, Show)
860940
861−data JSONOutputProp = JSONOutputProp { jsonopRecordDelimiter :: Maybe Text }
862− deriving (Eq, Show)
941+data JSONOutputProp = JSONOutputProp {jsonopRecordDelimiter :: Maybe Text}
942+ deriving (Eq, Show)
863943
864944-- | Set the output record delimiter for JSON format
865945outputJSONFromRecordDelimiter :: Text -> OutputSerialization
866946outputJSONFromRecordDelimiter t =
867− OutputSerializationJSON (JSONOutputProp $ Just t)
947+ OutputSerializationJSON (JSONOutputProp $ Just t)
868948
869949-- Response related types
870950
871951-- | An EventMessage represents each kind of message received from the server.
872−data EventMessage = ProgressEventMessage { emProgress :: Progress }
873− | StatsEventMessage { emStats :: Stats }
874− | RequestLevelErrorMessage { emErrorCode :: Text
875− , emErrorMessage :: Text
876− }
877− | RecordPayloadEventMessage { emPayloadBytes :: ByteString }
878− deriving (Eq, Show)
879−
880−data MsgHeaderName = MessageType
881− | EventType
882− | ContentType
883− | ErrorCode
884− | ErrorMessage
885− deriving (Eq, Show)
952+data EventMessage
953+ = ProgressEventMessage {emProgress :: Progress}
954+ | StatsEventMessage {emStats :: Stats}
955+ | RequestLevelErrorMessage
956+ { emErrorCode :: Text,
957+ emErrorMessage :: Text
958+ }
959+ | RecordPayloadEventMessage {emPayloadBytes :: ByteString}
960+ deriving (Eq, Show)
961+
962+data MsgHeaderName
963+ = MessageType
964+ | EventType
965+ | ContentType
966+ | ErrorCode
967+ | ErrorMessage
968+ deriving (Eq, Show)
886969
887970msgHeaderValueType :: Word8
888971msgHeaderValueType = 7
@@ -890,11 +973,12 @@ msgHeaderValueType = 7
890973type MessageHeader = (MsgHeaderName, Text)
891974
892975-- | Represent the progress event returned in the Select response.
893−data Progress = Progress { pBytesScanned :: Int64
894− , pBytesProcessed :: Int64
895− , pBytesReturned :: Int64
896− }
897− deriving (Eq, Show)
976+data Progress = Progress
977+ { pBytesScanned :: Int64,
978+ pBytesProcessed :: Int64,
979+ pBytesReturned :: Int64
980+ }
981+ deriving (Eq, Show)
898982
899983-- | Represent the stats event returned at the end of the Select
900984-- response.
@@ -907,43 +991,51 @@ type Stats = Progress
907991-- | Represents different kinds of payload that are used with S3 API
908992-- requests.
909993data Payload
910− = PayloadBS ByteString
911− | PayloadH Handle Int64 Int64 -- file handle, offset and length
912− | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
994+ = PayloadBS ByteString
995+ | PayloadH Handle Int64 Int64 -- file handle, offset and length
996+ | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
913997
914998defaultPayload :: Payload
915999defaultPayload = PayloadBS ""
9161000
917−data AdminReqInfo = AdminReqInfo {
918− ariMethod :: Method
919− , ariPayloadHash :: Maybe ByteString
920− , ariPayload :: Payload
921− , ariPath :: ByteString
922− , ariHeaders :: [Header]
923− , ariQueryParams :: Query
1001+data AdminReqInfo = AdminReqInfo
1002+ { ariMethod :: Method,
1003+ ariPayloadHash :: Maybe ByteString,
1004+ ariPayload :: Payload,
1005+ ariPath :: ByteString,
1006+ ariHeaders :: [Header],
1007+ ariQueryParams :: Query
9241008 }
9251009
9261010data S3ReqInfo = S3ReqInfo
927− { riMethod :: Method
928− , riBucket :: Maybe Bucket
929− , riObject :: Maybe Object
930− , riQueryParams :: Query
931− , riHeaders :: [Header]
932− , riPayload :: Payload
933− , riPayloadHash :: Maybe ByteString
934− , riRegion :: Maybe Region
935− , riNeedsLocation :: Bool
1011+ { riMethod :: Method,
1012+ riBucket :: Maybe Bucket,
1013+ riObject :: Maybe Object,
1014+ riQueryParams :: Query,
1015+ riHeaders :: [Header],
1016+ riPayload :: Payload,
1017+ riPayloadHash :: Maybe ByteString,
1018+ riRegion :: Maybe Region,
1019+ riNeedsLocation :: Bool
9361020 }
9371021
9381022defaultS3ReqInfo :: S3ReqInfo
939−defaultS3ReqInfo = S3ReqInfo HT.methodGet Nothing Nothing
940− [] [] defaultPayload Nothing Nothing True
1023+defaultS3ReqInfo =
1024+ S3ReqInfo
1025+ HT.methodGet
1026+ Nothing
1027+ Nothing
1028+ []
1029+ []
1030+ defaultPayload
1031+ Nothing
1032+ Nothing
1033+ True
9411034
9421035getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
9431036getS3Path b o =
944− let segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
945− in
946− B.concat ["/", B.intercalate "/" segments]
1037+ let segments = map toUtf8 $ catMaybes $ b : bool [] [o] (isJust b)
1038+ in B.concat ["/", B.intercalate "/" segments]
9471039
9481040-- | Time to expire for a presigned URL. It interpreted as a number of
9491041-- seconds. The maximum duration that can be specified is 7 days.
@@ -953,40 +1045,38 @@ type RegionMap = H.HashMap Bucket Region
9531045
9541046-- | The Minio Monad - all computations accessing object storage
9551047-- happens in it.
956−newtype Minio a = Minio {
957− unMinio :: ReaderT MinioConn (ResourceT IO) a
1048+newtype Minio a = Minio
1049+ { unMinio :: ReaderT MinioConn (ResourceT IO) a
9581050 }
959− deriving (
960− Functor
961− , Applicative
962− , Monad
963− , MonadIO
964− , MonadReader MinioConn
965− , MonadResource
1051+ deriving
1052+ ( Functor,
1053+ Applicative,
1054+ Monad,
1055+ MonadIO,
1056+ MonadReader MinioConn,
1057+ MonadResource,
1058+ MonadUnliftIO
9661059 )
9671060
968−instance MonadUnliftIO Minio where
969− askUnliftIO = Minio $ ReaderT $ \r ->
970− withUnliftIO $ \u ->
971− return (UnliftIO (unliftIO u . flip runReaderT r . unMinio))
972−
9731061-- | MinioConn holds connection info and a connection pool to allow
9741062-- for efficient resource re-use.
9751063data MinioConn = MinioConn
976− { mcConnInfo :: ConnectInfo
977− , mcConnManager :: NC.Manager
978− , mcRegionMap :: MVar RegionMap
1064+ { mcConnInfo :: ConnectInfo,
1065+ mcConnManager :: NC.Manager,
1066+ mcRegionMap :: MVar RegionMap
9791067 }
9801068
9811069class HasSvcNamespace env where
9821070 getSvcNamespace :: env -> Text
9831071
9841072instance HasSvcNamespace MinioConn where
985− getSvcNamespace env = let host = connectHost $ mcConnInfo env
986− in if | host == "storage.googleapis.com" ->
987− "http://doc.s3.amazonaws.com/2006-03-01"
988− | otherwise ->
989− "http://s3.amazonaws.com/doc/2006-03-01/"
1073+ getSvcNamespace env =
1074+ let host = connectHost $ mcConnInfo env
1075+ in if
1076+ | host == "storage.googleapis.com" ->
1077+ "http://doc.s3.amazonaws.com/2006-03-01"
1078+ | otherwise ->
1079+ "http://s3.amazonaws.com/doc/2006-03-01/"
9901080
9911081-- | Takes connection information and returns a connection object to
9921082-- be passed to 'runMinio'. The returned value can be kept in the
@@ -994,11 +1084,12 @@ instance HasSvcNamespace MinioConn where
9941084-- object storage is accessed.
9951085connect :: ConnectInfo -> IO MinioConn
9961086connect ci = do
997− let settings | connectIsSecure ci && connectDisableTLSCertValidation ci =
998− let badTlsSettings = Conn.TLSSettingsSimple True False False
999− in TLS.mkManagerSettings badTlsSettings Nothing
1000− | connectIsSecure ci = NC.tlsManagerSettings
1001− | otherwise = defaultManagerSettings
1087+ let settings
1088+ | connectIsSecure ci && connectDisableTLSCertValidation ci =
1089+ let badTlsSettings = Conn.TLSSettingsSimple True False False
1090+ in TLS.mkManagerSettings badTlsSettings Nothing
1091+ | connectIsSecure ci = NC.tlsManagerSettings
1092+ | otherwise = defaultManagerSettings
10021093 mgr <- NC.newManager settings
10031094 mkMinioConn ci mgr
10041095
@@ -1012,8 +1103,8 @@ runMinioWith conn m = runResourceT $ runMinioResWith conn m
10121103-- `MinioConn`.
10131104mkMinioConn :: ConnectInfo -> NC.Manager -> IO MinioConn
10141105mkMinioConn ci mgr = do
1015− rMapMVar <- M.newMVar H.empty
1016− return $ MinioConn ci mgr rMapMVar
1106+ rMapMVar <- M.newMVar H.empty
1107+ return $ MinioConn ci mgr rMapMVar
10171108
10181109-- | Run the Minio action and return the result or an error.
10191110runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
@@ -1026,12 +1117,12 @@ runMinio ci m = do
10261117runMinioResWith :: MinioConn -> Minio a -> ResourceT IO (Either MinioErr a)
10271118runMinioResWith conn m =
10281119 flip runReaderT conn . unMinio $
1029− fmap Right m `U.catches`
1030− [ U.Handler handlerServiceErr
1031− , U.Handler handlerHE
1032− , U.Handler handlerFE
1033− , U.Handler handlerValidation
1034− ]
1120+ fmap Right m
1121+ `U.catches` [ U.Handler handlerServiceErr,
1122+ U.Handler handlerHE,
1123+ U.Handler handlerFE,
1124+ U.Handler handlerValidation
1125+ ]
10351126 where
10361127 handlerServiceErr = return . Left . MErrService
10371128 handlerHE = return . Left . MErrHTTP
src/Network/Minio/Data/ByteString.hs+17−17
@@ -13,23 +13,22 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−
1716{-# LANGUAGE FlexibleInstances #-}
17+
1818module Network.Minio.Data.ByteString
19− (
20− stripBS
21− , UriEncodable(..)
22− ) where
19+ ( stripBS,
20+ UriEncodable (..),
21+ )
22+where
2323
2424import qualified Data.ByteString as B
2525import qualified Data.ByteString.Builder as BB
2626import qualified Data.ByteString.Char8 as BC8
2727import qualified Data.ByteString.Lazy as LB
28−import Data.Char (isSpace, toUpper, isAsciiUpper, isAsciiLower, isDigit)
28+import Data.Char (isAsciiLower, isAsciiUpper)
2929import qualified Data.Text as T
30−import Numeric (showHex)
31−
32−import Lib.Prelude
30+import Lib.Prelude
31+import Numeric (showHex)
3332
3433stripBS :: ByteString -> ByteString
3534stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
@@ -40,7 +39,7 @@ class UriEncodable s where
4039instance UriEncodable [Char] where
4140 uriEncode encodeSlash payload =
4241 LB.toStrict $ BB.toLazyByteString $ mconcat $
43− map (`uriEncodeChar` encodeSlash) payload
42+ map (`uriEncodeChar` encodeSlash) payload
4443
4544instance UriEncodable ByteString where
4645 -- assumes that uriEncode is passed ASCII encoded strings.
@@ -59,16 +58,17 @@ uriEncodeChar '/' True = BB.byteString "%2F"
5958uriEncodeChar '/' False = BB.char7 '/'
6059uriEncodeChar ch _
6160 | isAsciiUpper ch
62− || isAsciiLower ch
63− || isDigit ch
64− || (ch == '_')
65− || (ch == '-')
66− || (ch == '.')
67− || (ch == '~') = BB.char7 ch
61+ || isAsciiLower ch
62+ || isDigit ch
63+ || (ch == '_')
64+ || (ch == '-')
65+ || (ch == '.')
66+ || (ch == '~') =
67+ BB.char7 ch
6868 | otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
6969 where
7070 f :: Word8 -> BB.Builder
7171 f n = BB.char7 '%' <> BB.string7 hexStr
7272 where
7373 hexStr = map toUpper $ showHex q $ showHex r ""
74− (q, r) = divMod (fromIntegral n) (16::Word8)
74+ (q, r) = divMod (fromIntegral n) (16 :: Word8)
src/Network/Minio/Data/Crypto.hs+25−25
@@ -15,31 +15,31 @@
1515--
1616
1717module Network.Minio.Data.Crypto
18− (
19− hashSHA256
20− , hashSHA256FromSource
21−
22− , hashMD5
23− , hashMD5ToBase64
24− , hashMD5FromSource
25−
26− , hmacSHA256
27− , hmacSHA256RawBS
28− , digestToBS
29− , digestToBase16
30−
31− , encodeToBase64
32− ) where
33−
34−import Crypto.Hash (Digest, MD5 (..), SHA256 (..),
35− hashWith)
36−import Crypto.Hash.Conduit (sinkHash)
37−import Crypto.MAC.HMAC (HMAC, hmac)
38−import Data.ByteArray (ByteArrayAccess, convert)
39−import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
40−import qualified Data.Conduit as C
41−
42−import Lib.Prelude
18+ ( hashSHA256,
19+ hashSHA256FromSource,
20+ hashMD5,
21+ hashMD5ToBase64,
22+ hashMD5FromSource,
23+ hmacSHA256,
24+ hmacSHA256RawBS,
25+ digestToBS,
26+ digestToBase16,
27+ encodeToBase64,
28+ )
29+where
30+
31+import Crypto.Hash
32+ ( Digest,
33+ MD5 (..),
34+ SHA256 (..),
35+ hashWith,
36+ )
37+import Crypto.Hash.Conduit (sinkHash)
38+import Crypto.MAC.HMAC (HMAC, hmac)
39+import Data.ByteArray (ByteArrayAccess, convert)
40+import Data.ByteArray.Encoding (Base (Base16, Base64), convertToBase)
41+import qualified Data.Conduit as C
42+import Lib.Prelude
4343
4444hashSHA256 :: ByteString -> ByteString
4545hashSHA256 = digestToBase16 . hashWith SHA256
src/Network/Minio/Data/Time.hs+11−13
@@ -15,20 +15,18 @@
1515--
1616
1717module Network.Minio.Data.Time
18− (
19− awsTimeFormat
20− , awsTimeFormatBS
21− , awsDateFormat
22− , awsDateFormatBS
23− , awsParseTime
24− , iso8601TimeFormat
25− ) where
26−
27−
28−import Data.ByteString.Char8 (pack)
18+ ( awsTimeFormat,
19+ awsTimeFormatBS,
20+ awsDateFormat,
21+ awsDateFormatBS,
22+ awsParseTime,
23+ iso8601TimeFormat,
24+ )
25+where
26+
27+import Data.ByteString.Char8 (pack)
2928import qualified Data.Time as Time
30−
31−import Lib.Prelude
29+import Lib.Prelude
3230
3331awsTimeFormat :: UTCTime -> [Char]
3432awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
src/Network/Minio/Errors.hs+48−47
@@ -16,74 +16,75 @@
1616
1717module Network.Minio.Errors where
1818
19−import Control.Exception
19+import Control.Exception
20+import Lib.Prelude
2021import qualified Network.HTTP.Conduit as NC
2122
22−import Lib.Prelude
23−
24−
2523---------------------------------
2624-- Errors
2725---------------------------------
26+
2827-- | Various validation errors
29−data MErrV = MErrVSinglePUTSizeExceeded Int64
30− | MErrVPutSizeExceeded Int64
31− | MErrVETagHeaderNotFound
32− | MErrVInvalidObjectInfoResponse
33− | MErrVInvalidSrcObjSpec Text
34− | MErrVInvalidSrcObjByteRange (Int64, Int64)
35− | MErrVCopyObjSingleNoRangeAccepted
36− | MErrVRegionNotSupported Text
37− | MErrVXmlParse Text
38− | MErrVInvalidBucketName Text
39− | MErrVInvalidObjectName Text
40− | MErrVInvalidUrlExpiry Int
41− | MErrVJsonParse Text
42− | MErrVInvalidHealPath
43− | MErrVMissingCredentials
44− | MErrVInvalidEncryptionKeyLength
45− | MErrVStreamingBodyUnexpectedEOF
46− | MErrVUnexpectedPayload
28+data MErrV
29+ = MErrVSinglePUTSizeExceeded Int64
30+ | MErrVPutSizeExceeded Int64
31+ | MErrVETagHeaderNotFound
32+ | MErrVInvalidObjectInfoResponse
33+ | MErrVInvalidSrcObjSpec Text
34+ | MErrVInvalidSrcObjByteRange (Int64, Int64)
35+ | MErrVCopyObjSingleNoRangeAccepted
36+ | MErrVRegionNotSupported Text
37+ | MErrVXmlParse Text
38+ | MErrVInvalidBucketName Text
39+ | MErrVInvalidObjectName Text
40+ | MErrVInvalidUrlExpiry Int
41+ | MErrVJsonParse Text
42+ | MErrVInvalidHealPath
43+ | MErrVMissingCredentials
44+ | MErrVInvalidEncryptionKeyLength
45+ | MErrVStreamingBodyUnexpectedEOF
46+ | MErrVUnexpectedPayload
4747 deriving (Show, Eq)
4848
4949instance Exception MErrV
5050
5151-- | Errors returned by S3 compatible service
52−data ServiceErr = BucketAlreadyExists
53− | BucketAlreadyOwnedByYou
54− | NoSuchBucket
55− | InvalidBucketName
56− | NoSuchKey
57− | SelectErr Text Text
58− | ServiceErr Text Text
52+data ServiceErr
53+ = BucketAlreadyExists
54+ | BucketAlreadyOwnedByYou
55+ | NoSuchBucket
56+ | InvalidBucketName
57+ | NoSuchKey
58+ | SelectErr Text Text
59+ | ServiceErr Text Text
5960 deriving (Show, Eq)
6061
6162instance Exception ServiceErr
6263
6364toServiceErr :: Text -> Text -> ServiceErr
64−toServiceErr "NoSuchKey" _ = NoSuchKey
65−toServiceErr "NoSuchBucket" _ = NoSuchBucket
66−toServiceErr "InvalidBucketName" _ = InvalidBucketName
65+toServiceErr "NoSuchKey" _ = NoSuchKey
66+toServiceErr "NoSuchBucket" _ = NoSuchBucket
67+toServiceErr "InvalidBucketName" _ = InvalidBucketName
6768toServiceErr "BucketAlreadyOwnedByYou" _ = BucketAlreadyOwnedByYou
68−toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
69−toServiceErr code message = ServiceErr code message
70−
69+toServiceErr "BucketAlreadyExists" _ = BucketAlreadyExists
70+toServiceErr code message = ServiceErr code message
7171
7272-- | Errors thrown by the library
73−data MinioErr = MErrHTTP NC.HttpException
74− | MErrIO IOException
75− | MErrService ServiceErr
76− | MErrValidation MErrV
73+data MinioErr
74+ = MErrHTTP NC.HttpException
75+ | MErrIO IOException
76+ | MErrService ServiceErr
77+ | MErrValidation MErrV
7778 deriving (Show)
7879
7980instance Eq MinioErr where
80− MErrHTTP _ == MErrHTTP _ = True
81− MErrHTTP _ == _ = False
82− MErrIO _ == MErrIO _ = True
83− MErrIO _ == _ = False
84− MErrService a == MErrService b = a == b
85− MErrService _ == _ = False
86− MErrValidation a == MErrValidation b = a == b
87− MErrValidation _ == _ = False
81+ MErrHTTP _ == MErrHTTP _ = True
82+ MErrHTTP _ == _ = False
83+ MErrIO _ == MErrIO _ = True
84+ MErrIO _ == _ = False
85+ MErrService a == MErrService b = a == b
86+ MErrService _ == _ = False
87+ MErrValidation a == MErrValidation b = a == b
88+ MErrValidation _ == _ = False
8889
8990instance Exception MinioErr
src/Network/Minio/JsonParser.hs+23−16
@@ -15,28 +15,35 @@
1515--
1616
1717module Network.Minio.JsonParser
18− (
19− parseErrResponseJSON
20− ) where
18+ ( parseErrResponseJSON,
19+ )
20+where
2121
22−import Data.Aeson (FromJSON, eitherDecode, parseJSON,
23− withObject, (.:))
24−import qualified Data.Text as T
22+import Data.Aeson
23+ ( (.:),
24+ FromJSON,
25+ eitherDecode,
26+ parseJSON,
27+ withObject,
28+ )
29+import qualified Data.Text as T
30+import Lib.Prelude
31+import Network.Minio.Errors
2532
26−import Lib.Prelude
33+data AdminErrJSON = AdminErrJSON
34+ { aeCode :: Text,
35+ aeMessage :: Text
36+ }
37+ deriving (Eq, Show)
2738
28−import Network.Minio.Errors
29−
30−data AdminErrJSON = AdminErrJSON { aeCode :: Text
31− , aeMessage :: Text
32− } deriving (Eq, Show)
3339instance FromJSON AdminErrJSON where
34− parseJSON = withObject "AdminErrJSON" $ \v -> AdminErrJSON
35− <$> v .: "Code"
36− <*> v .: "Message"
40+ parseJSON = withObject "AdminErrJSON" $ \v ->
41+ AdminErrJSON
42+ <$> v .: "Code"
43+ <*> v .: "Message"
3744
3845parseErrResponseJSON :: (MonadIO m) => LByteString -> m ServiceErr
3946parseErrResponseJSON jsondata =
4047 case eitherDecode jsondata of
4148 Right aErr -> return $ toServiceErr (aeCode aErr) (aeMessage aErr)
42− Left err -> throwIO $ MErrVJsonParse $ T.pack err
49+ Left err -> throwIO $ MErrVJsonParse $ T.pack err
src/Network/Minio/ListOps.hs+65−40
@@ -16,20 +16,19 @@
1616
1717module Network.Minio.ListOps where
1818
19−import qualified Data.Conduit as C
19+import qualified Data.Conduit as C
2020import qualified Data.Conduit.Combinators as CC
21−import qualified Data.Conduit.List as CL
22−
23−import Lib.Prelude
24−
25−import Network.Minio.Data
26−import Network.Minio.S3API
21+import qualified Data.Conduit.List as CL
22+import Lib.Prelude
23+import Network.Minio.Data
24+import Network.Minio.S3API
2725
2826-- | Represents a list output item - either an object or an object
2927-- prefix (i.e. a directory).
30−data ListItem = ListItemObject ObjectInfo
31− | ListItemPrefix Text
32− deriving (Show, Eq)
28+data ListItem
29+ = ListItemObject ObjectInfo
30+ | ListItemPrefix Text
31+ deriving (Show, Eq)
3332
3433-- | @'listObjects' bucket prefix recurse@ lists objects in a bucket
3534-- similar to a file system tree traversal.
@@ -48,73 +47,99 @@ listObjects bucket prefix recurse = loop Nothing
4847 where
4948 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
5049 loop nextToken = do
51− let
52− delimiter = bool (Just "/") Nothing recurse
50+ let delimiter = bool (Just "/") Nothing recurse
5351
5452 res <- lift $ listObjects' bucket prefix nextToken delimiter Nothing
5553 CL.sourceList $ map ListItemObject $ lorObjects res
56− unless recurse $
57− CL.sourceList $ map ListItemPrefix $ lorCPrefixes res
54+ unless recurse
55+ $ CL.sourceList
56+ $ map ListItemPrefix
57+ $ lorCPrefixes res
5858 when (lorHasMore res) $
5959 loop (lorNextToken res)
6060
6161-- | Lists objects - similar to @listObjects@, however uses the older
6262-- V1 AWS S3 API. Prefer @listObjects@ to this.
63−listObjectsV1 :: Bucket -> Maybe Text -> Bool
64− -> C.ConduitM () ListItem Minio ()
63+listObjectsV1 ::
64+ Bucket ->
65+ Maybe Text ->
66+ Bool ->
67+ C.ConduitM () ListItem Minio ()
6568listObjectsV1 bucket prefix recurse = loop Nothing
6669 where
6770 loop :: Maybe Text -> C.ConduitM () ListItem Minio ()
6871 loop nextMarker = do
69− let
70− delimiter = bool (Just "/") Nothing recurse
72+ let delimiter = bool (Just "/") Nothing recurse
7173
7274 res <- lift $ listObjectsV1' bucket prefix nextMarker delimiter Nothing
7375 CL.sourceList $ map ListItemObject $ lorObjects' res
74− unless recurse $
75− CL.sourceList $ map ListItemPrefix $ lorCPrefixes' res
76+ unless recurse
77+ $ CL.sourceList
78+ $ map ListItemPrefix
79+ $ lorCPrefixes' res
7680 when (lorHasMore' res) $
7781 loop (lorNextMarker res)
7882
7983-- | List incomplete uploads in a bucket matching the given prefix. If
8084-- recurse is set to True incomplete uploads for the given prefix are
8185-- recursively listed.
82−listIncompleteUploads :: Bucket -> Maybe Text -> Bool
83− -> C.ConduitM () UploadInfo Minio ()
86+listIncompleteUploads ::
87+ Bucket ->
88+ Maybe Text ->
89+ Bool ->
90+ C.ConduitM () UploadInfo Minio ()
8491listIncompleteUploads bucket prefix recurse = loop Nothing Nothing
8592 where
8693 loop :: Maybe Text -> Maybe Text -> C.ConduitM () UploadInfo Minio ()
8794 loop nextKeyMarker nextUploadIdMarker = do
88− let
89− delimiter = bool (Just "/") Nothing recurse
90−
91− res <- lift $ listIncompleteUploads' bucket prefix delimiter
92− nextKeyMarker nextUploadIdMarker Nothing
95+ let delimiter = bool (Just "/") Nothing recurse
96+
97+ res <-
98+ lift $
99+ listIncompleteUploads'
100+ bucket
101+ prefix
102+ delimiter
103+ nextKeyMarker
104+ nextUploadIdMarker
105+ Nothing
93106
94107 aggrSizes <- lift $ forM (lurUploads res) $ \(uKey, uId, _) -> do
95− partInfos <- C.runConduit $ listIncompleteParts bucket uKey uId
96− C..| CC.sinkList
97− return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
98−
99− CL.sourceList $
100− map (\((uKey, uId, uInitTime), size) ->
101− UploadInfo uKey uId uInitTime size
102− ) $ zip (lurUploads res) aggrSizes
108+ partInfos <-
109+ C.runConduit $
110+ listIncompleteParts bucket uKey uId
111+ C..| CC.sinkList
112+ return $ foldl (\sizeSofar p -> opiSize p + sizeSofar) 0 partInfos
113+
114+ CL.sourceList
115+ $ map
116+ ( \((uKey, uId, uInitTime), size) ->
117+ UploadInfo uKey uId uInitTime size
118+ )
119+ $ zip (lurUploads res) aggrSizes
103120
104121 when (lurHasMore res) $
105122 loop (lurNextKey res) (lurNextUpload res)
106123
107−
108124-- | List object parts of an ongoing multipart upload for given
109125-- bucket, object and uploadId.
110−listIncompleteParts :: Bucket -> Object -> UploadId
111− -> C.ConduitM () ObjectPartInfo Minio ()
126+listIncompleteParts ::
127+ Bucket ->
128+ Object ->
129+ UploadId ->
130+ C.ConduitM () ObjectPartInfo Minio ()
112131listIncompleteParts bucket object uploadId = loop Nothing
113132 where
114133 loop :: Maybe Text -> C.ConduitM () ObjectPartInfo Minio ()
115134 loop nextPartMarker = do
116− res <- lift $ listIncompleteParts' bucket object uploadId Nothing
117− nextPartMarker
135+ res <-
136+ lift $
137+ listIncompleteParts'
138+ bucket
139+ object
140+ uploadId
141+ Nothing
142+ nextPartMarker
118143 CL.sourceList $ lprParts res
119144 when (lprHasMore res) $
120145 loop (show <$> lprNextPart res)
src/Network/Minio/PresignedOperations.hs+201−145
@@ -15,43 +15,40 @@
1515--
1616
1717module Network.Minio.PresignedOperations
18− ( UrlExpiry
19− , makePresignedUrl
20− , presignedPutObjectUrl
21− , presignedGetObjectUrl
22− , presignedHeadObjectUrl
23−
24− , PostPolicyCondition(..)
25− , ppCondBucket
26− , ppCondContentLengthRange
27− , ppCondContentType
28− , ppCondKey
29− , ppCondKeyStartsWith
30− , ppCondSuccessActionStatus
31−
32− , PostPolicy(..)
33− , PostPolicyError(..)
34− , newPostPolicy
35− , showPostPolicy
36− , presignedPostPolicy
37− ) where
38−
39−import Data.Aeson ((.=))
40−import qualified Data.Aeson as Json
41−import Data.ByteString.Builder (byteString, toLazyByteString)
42−import qualified Data.HashMap.Strict as H
43−import qualified Data.Text as T
44−import qualified Data.Time as Time
45−import qualified Network.HTTP.Conduit as NC
46−import qualified Network.HTTP.Types as HT
47−import Network.HTTP.Types.Header (hHost)
48−
49−import Lib.Prelude
50−
51−import Network.Minio.Data
52−import Network.Minio.Data.Time
53−import Network.Minio.Errors
54−import Network.Minio.Sign.V4
18+ ( UrlExpiry,
19+ makePresignedUrl,
20+ presignedPutObjectUrl,
21+ presignedGetObjectUrl,
22+ presignedHeadObjectUrl,
23+ PostPolicyCondition (..),
24+ ppCondBucket,
25+ ppCondContentLengthRange,
26+ ppCondContentType,
27+ ppCondKey,
28+ ppCondKeyStartsWith,
29+ ppCondSuccessActionStatus,
30+ PostPolicy (..),
31+ PostPolicyError (..),
32+ newPostPolicy,
33+ showPostPolicy,
34+ presignedPostPolicy,
35+ )
36+where
37+
38+import Data.Aeson ((.=))
39+import qualified Data.Aeson as Json
40+import Data.ByteString.Builder (byteString, toLazyByteString)
41+import qualified Data.HashMap.Strict as H
42+import qualified Data.Text as T
43+import qualified Data.Time as Time
44+import Lib.Prelude
45+import qualified Network.HTTP.Conduit as NC
46+import qualified Network.HTTP.Types as HT
47+import Network.HTTP.Types.Header (hHost)
48+import Network.Minio.Data
49+import Network.Minio.Data.Time
50+import Network.Minio.Errors
51+import Network.Minio.Sign.V4
5552
5653-- | Generate a presigned URL. This function allows for advanced usage
5754-- - for simple cases prefer the `presigned*Url` functions.
@@ -61,42 +58,56 @@ import Network.Minio.Sign.V4
6158--
6259-- All extra query parameters or headers are signed, and therefore are
6360-- required to be sent when the generated URL is actually used.
64−makePresignedUrl :: UrlExpiry -> HT.Method -> Maybe Bucket -> Maybe Object
65− -> Maybe Region -> HT.Query -> HT.RequestHeaders
66− -> Minio ByteString
61+makePresignedUrl ::
62+ UrlExpiry ->
63+ HT.Method ->
64+ Maybe Bucket ->
65+ Maybe Object ->
66+ Maybe Region ->
67+ HT.Query ->
68+ HT.RequestHeaders ->
69+ Minio ByteString
6770makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
68− when (expiry > 7*24*3600 || expiry < 0) $
69− throwIO $ MErrVInvalidUrlExpiry expiry
71+ when (expiry > 7 * 24 * 3600 || expiry < 0)
72+ $ throwIO
73+ $ MErrVInvalidUrlExpiry expiry
7074
7175 ci <- asks mcConnInfo
7276
73− let
74− hostHeader = (hHost, getHostAddr ci)
75− req = NC.defaultRequest {
76− NC.method = method
77− , NC.secure = connectIsSecure ci
78− , NC.host = encodeUtf8 $ connectHost ci
79− , NC.port = connectPort ci
80− , NC.path = getS3Path bucket object
81− , NC.requestHeaders = hostHeader : extraHeaders
82− , NC.queryString = HT.renderQuery True extraQuery
83− }
77+ let hostHeader = (hHost, getHostAddr ci)
78+ req =
79+ NC.defaultRequest
80+ { NC.method = method,
81+ NC.secure = connectIsSecure ci,
82+ NC.host = encodeUtf8 $ connectHost ci,
83+ NC.port = connectPort ci,
84+ NC.path = getS3Path bucket object,
85+ NC.requestHeaders = hostHeader : extraHeaders,
86+ NC.queryString = HT.renderQuery True extraQuery
87+ }
8488 ts <- liftIO Time.getCurrentTime
8589
86− let sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
87− ts region (Just expiry) Nothing
88−
90+ let sp =
91+ SignParams
92+ (connectAccessKey ci)
93+ (connectSecretKey ci)
94+ ts
95+ region
96+ (Just expiry)
97+ Nothing
8998 signPairs = signV4 sp req
90−
9199 qpToAdd = (fmap . fmap) Just signPairs
92− queryStr = HT.renderQueryBuilder True
93− ((HT.parseQuery $ NC.queryString req) ++ qpToAdd)
100+ queryStr =
101+ HT.renderQueryBuilder
102+ True
103+ ((HT.parseQuery $ NC.queryString req) ++ qpToAdd)
94104 scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
95105
96− return $ toS $ toLazyByteString $ scheme
97− <> byteString (getHostAddr ci)
98− <> byteString (getS3Path bucket object)
99− <> queryStr
106+ return $ toStrictBS $ toLazyByteString $
107+ scheme
108+ <> byteString (getHostAddr ci)
109+ <> byteString (getS3Path bucket object)
110+ <> queryStr
100111
101112-- | Generate a URL with authentication signature to PUT (upload) an
102113-- object. Any extra headers if passed, are signed, and so they are
@@ -105,12 +116,22 @@ makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
105116--
106117-- For a list of possible headers to pass, please refer to the PUT
107118-- object REST API AWS S3 documentation.
108−presignedPutObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.RequestHeaders
109− -> Minio ByteString
119+presignedPutObjectUrl ::
120+ Bucket ->
121+ Object ->
122+ UrlExpiry ->
123+ HT.RequestHeaders ->
124+ Minio ByteString
110125presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
111126 region <- asks (Just . connectRegion . mcConnInfo)
112− makePresignedUrl expirySeconds HT.methodPut
113− (Just bucket) (Just object) region [] extraHeaders
127+ makePresignedUrl
128+ expirySeconds
129+ HT.methodPut
130+ (Just bucket)
131+ (Just object)
132+ region
133+ []
134+ extraHeaders
114135
115136-- | Generate a URL with authentication signature to GET (download) an
116137-- object. All extra query parameters and headers passed here will be
@@ -121,12 +142,23 @@ presignedPutObjectUrl bucket object expirySeconds extraHeaders = do
121142--
122143-- For a list of possible request parameters and headers, please refer
123144-- to the GET object REST API AWS S3 documentation.
124−presignedGetObjectUrl :: Bucket -> Object -> UrlExpiry -> HT.Query
125− -> HT.RequestHeaders -> Minio ByteString
145+presignedGetObjectUrl ::
146+ Bucket ->
147+ Object ->
148+ UrlExpiry ->
149+ HT.Query ->
150+ HT.RequestHeaders ->
151+ Minio ByteString
126152presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
127153 region <- asks (Just . connectRegion . mcConnInfo)
128− makePresignedUrl expirySeconds HT.methodGet
129− (Just bucket) (Just object) region extraQuery extraHeaders
154+ makePresignedUrl
155+ expirySeconds
156+ HT.methodGet
157+ (Just bucket)
158+ (Just object)
159+ region
160+ extraQuery
161+ extraHeaders
130162
131163-- | Generate a URL with authentication signature to make a HEAD
132164-- request on an object. This is used to fetch metadata about an
@@ -135,18 +167,29 @@ presignedGetObjectUrl bucket object expirySeconds extraQuery extraHeaders = do
135167--
136168-- For a list of possible headers to pass, please refer to the HEAD
137169-- object REST API AWS S3 documentation.
138−presignedHeadObjectUrl :: Bucket -> Object -> UrlExpiry
139− -> HT.RequestHeaders -> Minio ByteString
170+presignedHeadObjectUrl ::
171+ Bucket ->
172+ Object ->
173+ UrlExpiry ->
174+ HT.RequestHeaders ->
175+ Minio ByteString
140176presignedHeadObjectUrl bucket object expirySeconds extraHeaders = do
141177 region <- asks (Just . connectRegion . mcConnInfo)
142− makePresignedUrl expirySeconds HT.methodHead
143− (Just bucket) (Just object) region [] extraHeaders
178+ makePresignedUrl
179+ expirySeconds
180+ HT.methodHead
181+ (Just bucket)
182+ (Just object)
183+ region
184+ []
185+ extraHeaders
144186
145187-- | Represents individual conditions in a Post Policy document.
146−data PostPolicyCondition = PPCStartsWith Text Text
147− | PPCEquals Text Text
148− | PPCRange Text Int64 Int64
149− deriving (Show, Eq)
188+data PostPolicyCondition
189+ = PPCStartsWith Text Text
190+ | PPCEquals Text Text
191+ | PPCRange Text Int64 Int64
192+ deriving (Show, Eq)
150193
151194instance Json.ToJSON PostPolicyCondition where
152195 toJSON (PPCStartsWith k v) = Json.toJSON ["starts-with", k, v]
@@ -160,25 +203,28 @@ instance Json.ToJSON PostPolicyCondition where
160203 Json.foldable [Json.toJSON k, Json.toJSON minVal, Json.toJSON maxVal]
161204
162205-- | A PostPolicy is required to perform uploads via browser forms.
163−data PostPolicy = PostPolicy {
164− expiration :: UTCTime
165− , conditions :: [PostPolicyCondition]
166− } deriving (Show, Eq)
206+data PostPolicy = PostPolicy
207+ { expiration :: UTCTime,
208+ conditions :: [PostPolicyCondition]
209+ }
210+ deriving (Show, Eq)
167211
168212instance Json.ToJSON PostPolicy where
169213 toJSON (PostPolicy e c) =
170− Json.object $ [ "expiration" .= iso8601TimeFormat e
171− , "conditions" .= c
172− ]
214+ Json.object $
215+ [ "expiration" .= iso8601TimeFormat e,
216+ "conditions" .= c
217+ ]
173218 toEncoding (PostPolicy e c) =
174219 Json.pairs ("expiration" .= iso8601TimeFormat e <> "conditions" .= c)
175220
176221-- | Possible validation errors when creating a PostPolicy.
177−data PostPolicyError = PPEKeyNotSpecified
178− | PPEBucketNotSpecified
179− | PPEConditionKeyEmpty
180− | PPERangeInvalid
181− deriving (Eq, Show)
222+data PostPolicyError
223+ = PPEKeyNotSpecified
224+ | PPEBucketNotSpecified
225+ | PPEConditionKeyEmpty
226+ | PPERangeInvalid
227+ deriving (Eq, Show)
182228
183229-- | Set the bucket name that the upload should use.
184230ppCondBucket :: Bucket -> PostPolicyCondition
@@ -186,8 +232,10 @@ ppCondBucket = PPCEquals "bucket"
186232
187233-- | Set the content length range constraint with minimum and maximum
188234-- byte count values.
189−ppCondContentLengthRange :: Int64 -> Int64
190− -> PostPolicyCondition
235+ppCondContentLengthRange ::
236+ Int64 ->
237+ Int64 ->
238+ PostPolicyCondition
191239ppCondContentLengthRange = PPCRange "content-length-range"
192240
193241-- | Set the content-type header for the upload.
@@ -210,83 +258,91 @@ ppCondSuccessActionStatus n =
210258
211259-- | This function creates a PostPolicy after validating its
212260-- arguments.
213−newPostPolicy :: UTCTime -> [PostPolicyCondition]
214− -> Either PostPolicyError PostPolicy
261+newPostPolicy ::
262+ UTCTime ->
263+ [PostPolicyCondition] ->
264+ Either PostPolicyError PostPolicy
215265newPostPolicy expirationTime conds
216266 -- object name condition must be present
217267 | not $ any (keyEquals "key") conds =
218− Left PPEKeyNotSpecified
219−
268+ Left PPEKeyNotSpecified
220269 -- bucket name condition must be present
221270 | not $ any (keyEquals "bucket") conds =
222− Left PPEBucketNotSpecified
223−
271+ Left PPEBucketNotSpecified
224272 -- a condition with an empty key is invalid
225273 | any (keyEquals "") conds || any isEmptyRangeKey conds =
226− Left PPEConditionKeyEmpty
227−
274+ Left PPEConditionKeyEmpty
228275 -- invalid range check
229276 | any isInvalidRange conds =
230− Left PPERangeInvalid
231−
277+ Left PPERangeInvalid
232278 -- all good!
233279 | otherwise =
234− return $ PostPolicy expirationTime conds
235−
280+ return $ PostPolicy expirationTime conds
236281 where
237282 keyEquals k' (PPCStartsWith k _) = k == k'
238− keyEquals k' (PPCEquals k _) = k == k'
239− keyEquals _ _ = False
240−
283+ keyEquals k' (PPCEquals k _) = k == k'
284+ keyEquals _ _ = False
241285 isEmptyRangeKey (PPCRange k _ _) = k == ""
242− isEmptyRangeKey _ = False
243−
286+ isEmptyRangeKey _ = False
244287 isInvalidRange (PPCRange _ mi ma) = mi < 0 || mi > ma
245− isInvalidRange _ = False
288+ isInvalidRange _ = False
246289
247290-- | Convert Post Policy to a string (e.g. for printing).
248291showPostPolicy :: PostPolicy -> ByteString
249−showPostPolicy = toS . Json.encode
292+showPostPolicy = toStrictBS . Json.encode
250293
251294-- | Generate a presigned URL and POST policy to upload files via a
252295-- browser. On success, this function returns a URL and POST
253296-- form-data.
254−presignedPostPolicy :: PostPolicy
255− -> Minio (ByteString, H.HashMap Text ByteString)
297+presignedPostPolicy ::
298+ PostPolicy ->
299+ Minio (ByteString, H.HashMap Text ByteString)
256300presignedPostPolicy p = do
257301 ci <- asks mcConnInfo
258302 signTime <- liftIO $ Time.getCurrentTime
259303
260− let
261− extraConditions =
262− [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime)
263− , PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256"
264− , PPCEquals "x-amz-credential"
265− (T.intercalate "/" [connectAccessKey ci,
266− decodeUtf8 $ mkScope signTime region])
267− ]
268− ppWithCreds = p {
269− conditions = conditions p ++ extraConditions
270− }
271− sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
272− signTime (Just $ connectRegion ci) Nothing Nothing
273− signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp
274−
275−
276− -- compute form-data
277− mkPair (PPCStartsWith k v) = Just (k, v)
278− mkPair (PPCEquals k v) = Just (k, v)
279− mkPair _ = Nothing
280− formFromPolicy = H.map toS $ H.fromList $ catMaybes $
281− mkPair <$> conditions ppWithCreds
282− formData = formFromPolicy `H.union` signData
283−
284− -- compute POST upload URL
285− bucket = H.lookupDefault "" "bucket" formData
286− scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
287− region = connectRegion ci
288−
289− url = toS $ toLazyByteString $ scheme <> byteString (getHostAddr ci) <>
290− byteString "/" <> byteString (toS bucket) <> byteString "/"
304+ let extraConditions =
305+ [ PPCEquals "x-amz-date" (toS $ awsTimeFormat signTime),
306+ PPCEquals "x-amz-algorithm" "AWS4-HMAC-SHA256",
307+ PPCEquals
308+ "x-amz-credential"
309+ ( T.intercalate
310+ "/"
311+ [ connectAccessKey ci,
312+ decodeUtf8 $ mkScope signTime region
313+ ]
314+ )
315+ ]
316+ ppWithCreds =
317+ p
318+ { conditions = conditions p ++ extraConditions
319+ }
320+ sp =
321+ SignParams
322+ (connectAccessKey ci)
323+ (connectSecretKey ci)
324+ signTime
325+ (Just $ connectRegion ci)
326+ Nothing
327+ Nothing
328+ signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp
329+ -- compute form-data
330+ mkPair (PPCStartsWith k v) = Just (k, v)
331+ mkPair (PPCEquals k v) = Just (k, v)
332+ mkPair _ = Nothing
333+ formFromPolicy =
334+ H.map toUtf8 $ H.fromList $ catMaybes $
335+ mkPair <$> conditions ppWithCreds
336+ formData = formFromPolicy `H.union` signData
337+ -- compute POST upload URL
338+ bucket = H.lookupDefault "" "bucket" formData
339+ scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
340+ region = connectRegion ci
341+ url =
342+ toStrictBS $ toLazyByteString $
343+ scheme <> byteString (getHostAddr ci)
344+ <> byteString "/"
345+ <> byteString bucket
346+ <> byteString "/"
291347
292348 return (url, formData)
src/Network/Minio/PutObject.hs+84−68
@@ -15,29 +15,24 @@
1515--
1616
1717module Network.Minio.PutObject
18− (
19− putObjectInternal
20− , ObjectData(..)
21− , selectPartSizes
22− ) where
23−
24−
25−import Conduit (takeC)
26−import qualified Conduit as C
27−import qualified Data.ByteString.Lazy as LBS
28−import qualified Data.Conduit.Binary as CB
18+ ( putObjectInternal,
19+ ObjectData (..),
20+ selectPartSizes,
21+ )
22+where
23+
24+import Conduit (takeC)
25+import qualified Conduit as C
26+import qualified Data.ByteString.Lazy as LBS
27+import qualified Data.Conduit.Binary as CB
2928import qualified Data.Conduit.Combinators as CC
30−import qualified Data.Conduit.List as CL
31−import qualified Data.List as List
32−
33−
34−import Lib.Prelude
35−
36−import Network.Minio.Data
37−import Network.Minio.Errors
38−import Network.Minio.S3API
39−import Network.Minio.Utils
40−
29+import qualified Data.Conduit.List as CL
30+import qualified Data.List as List
31+import Lib.Prelude
32+import Network.Minio.Data
33+import Network.Minio.Errors
34+import Network.Minio.S3API
35+import Network.Minio.Utils
4136
4237-- | A data-type to represent the source data for an object. A
4338-- file-path or a producer-conduit may be provided.
@@ -50,37 +45,45 @@ import Network.Minio.Utils
5045-- the input - if it is not provided, upload will continue until the
5146-- stream ends or the object reaches `maxObjectSize` size.
5247data ObjectData m
53− = ODFile FilePath (Maybe Int64) -- ^ Takes filepath and optional
54− -- size.
55− | ODStream (C.ConduitM () ByteString m ()) (Maybe Int64) -- ^ Pass
56− -- size
57− -- (bytes)
58− -- if
59− -- known.
48+ = -- | Takes filepath and optional
49+ -- size.
50+ ODFile FilePath (Maybe Int64)
51+ | -- | Pass
52+ -- size
53+ -- (bytes)
54+ -- if
55+ -- known.
56+ ODStream (C.ConduitM () ByteString m ()) (Maybe Int64)
6057
6158-- | Put an object from ObjectData. This high-level API handles
6259-- objects of all sizes, and even if the object size is unknown.
63−putObjectInternal :: Bucket -> Object -> PutObjectOptions
64− -> ObjectData Minio -> Minio ETag
60+putObjectInternal ::
61+ Bucket ->
62+ Object ->
63+ PutObjectOptions ->
64+ ObjectData Minio ->
65+ Minio ETag
6566putObjectInternal b o opts (ODStream src sizeMay) = do
6667 case sizeMay of
6768 -- unable to get size, so assume non-seekable file
6869 Nothing -> sequentialMultipartUpload b o opts Nothing src
69−
7070 -- got file size, so check for single/multipart upload
7171 Just size ->
72− if | size <= 64 * oneMiB -> do
73− bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
74− putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
75− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
76− | otherwise -> sequentialMultipartUpload b o opts (Just size) src
77−
72+ if
73+ | size <= 64 * oneMiB -> do
74+ bs <- C.runConduit $ src C..| takeC (fromIntegral size) C..| CB.sinkLbs
75+ putObjectSingle' b o (pooToHeaders opts) $ LBS.toStrict bs
76+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
77+ | otherwise -> sequentialMultipartUpload b o opts (Just size) src
7878putObjectInternal b o opts (ODFile fp sizeMay) = do
7979 hResE <- withNewHandle fp $ \h ->
8080 liftM2 (,) (isHandleSeekable h) (getFileSize h)
8181
82− (isSeekable, handleSizeMay) <- either (const $ return (False, Nothing)) return
83− hResE
82+ (isSeekable, handleSizeMay) <-
83+ either
84+ (const $ return (False, Nothing))
85+ return
86+ hResE
8487
8588 -- prefer given size to queried size.
8689 let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
@@ -88,18 +91,25 @@ putObjectInternal b o opts (ODFile fp sizeMay) = do
8891 case finalSizeMay of
8992 -- unable to get size, so assume non-seekable file
9093 Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
91−
9294 -- got file size, so check for single/multipart upload
9395 Just size ->
94− if | size <= 64 * oneMiB -> either throwIO return =<<
95− withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
96− | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
97− | isSeekable -> parallelMultipartUpload b o opts fp size
98− | otherwise -> sequentialMultipartUpload b o opts (Just size) $
99− CB.sourceFile fp
100−
101−parallelMultipartUpload :: Bucket -> Object -> PutObjectOptions
102− -> FilePath -> Int64 -> Minio ETag
96+ if
97+ | size <= 64 * oneMiB ->
98+ either throwIO return
99+ =<< withNewHandle fp (\h -> putObjectSingle b o (pooToHeaders opts) h 0 size)
100+ | size > maxObjectSize -> throwIO $ MErrVPutSizeExceeded size
101+ | isSeekable -> parallelMultipartUpload b o opts fp size
102+ | otherwise ->
103+ sequentialMultipartUpload b o opts (Just size) $
104+ CB.sourceFile fp
105+
106+parallelMultipartUpload ::
107+ Bucket ->
108+ Object ->
109+ PutObjectOptions ->
110+ FilePath ->
111+ Int64 ->
112+ Minio ETag
103113parallelMultipartUpload b o opts filePath size = do
104114 -- get a new upload id.
105115 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -109,15 +119,17 @@ parallelMultipartUpload b o opts filePath size = do
109119 let threads = fromMaybe 10 $ pooNumThreads opts
110120
111121 -- perform upload with 'threads' threads
112− uploadedPartsE <- limitedMapConcurrently (fromIntegral threads)
113− (uploadPart uploadId) partSizeInfo
122+ uploadedPartsE <-
123+ limitedMapConcurrently
124+ (fromIntegral threads)
125+ (uploadPart uploadId)
126+ partSizeInfo
114127
115128 -- if there were any errors, rethrow exception.
116129 mapM_ throwIO $ lefts uploadedPartsE
117130
118131 -- if we get here, all parts were successfully uploaded.
119132 completeMultipartUpload b o uploadId $ rights uploadedPartsE
120−
121133 where
122134 uploadPart uploadId (partNum, offset, sz) =
123135 withNewHandle filePath $ \h -> do
@@ -125,10 +137,13 @@ parallelMultipartUpload b o opts filePath size = do
125137 putObjectPart b o uploadId partNum [] payload
126138
127139-- | Upload multipart object from conduit source sequentially
128−sequentialMultipartUpload :: Bucket -> Object -> PutObjectOptions
129− -> Maybe Int64
130− -> C.ConduitM () ByteString Minio ()
131− -> Minio ETag
140+sequentialMultipartUpload ::
141+ Bucket ->
142+ Object ->
143+ PutObjectOptions ->
144+ Maybe Int64 ->
145+ C.ConduitM () ByteString Minio () ->
146+ Minio ETag
132147sequentialMultipartUpload b o opts sizeMay src = do
133148 -- get a new upload id.
134149 uploadId <- newMultipartUpload b o (pooToHeaders opts)
@@ -136,22 +151,23 @@ sequentialMultipartUpload b o opts sizeMay src = do
136151 -- upload parts in loop
137152 let partSizes = selectPartSizes $ maybe maxObjectSize identity sizeMay
138153 (pnums, _, sizes) = List.unzip3 partSizes
139− uploadedParts <- C.runConduit
140− $ src
141− C..| chunkBSConduit (map fromIntegral sizes)
142− C..| CL.map PayloadBS
143− C..| uploadPart' uploadId pnums
144− C..| CC.sinkList
154+ uploadedParts <-
155+ C.runConduit $
156+ src
157+ C..| chunkBSConduit (map fromIntegral sizes)
158+ C..| CL.map PayloadBS
159+ C..| uploadPart' uploadId pnums
160+ C..| CC.sinkList
145161
146162 -- complete multipart upload
147163 completeMultipartUpload b o uploadId uploadedParts
148−
149164 where
150165 uploadPart' _ [] = return ()
151− uploadPart' uid (pn:pns) = do
166+ uploadPart' uid (pn : pns) = do
152167 payloadMay <- C.await
153168 case payloadMay of
154169 Nothing -> return ()
155− Just payload -> do pinfo <- lift $ putObjectPart b o uid pn [] payload
156− C.yield pinfo
157− uploadPart' uid pns
170+ Just payload -> do
171+ pinfo <- lift $ putObjectPart b o uid pn [] payload
172+ C.yield pinfo
173+ uploadPart' uid pns
src/Network/Minio/S3API.hs+455−322
@@ -15,150 +15,162 @@
1515--
1616
1717module Network.Minio.S3API
18− (
19− Region
20− , getLocation
21−
22− -- * Listing buckets
23− --------------------
24− , getService
25−
26− -- * Listing objects
27− --------------------
28− , ListObjectsResult(..)
29− , ListObjectsV1Result(..)
30− , listObjects'
31− , listObjectsV1'
32−
33− -- * Retrieving buckets
34− , headBucket
35−
36− -- * Retrieving objects
37− -----------------------
38− , getObject'
39− , headObject
40−
41− -- * Creating buckets and objects
42− ---------------------------------
43− , putBucket
44− , ETag
45− , maxSinglePutObjectSizeBytes
46− , putObjectSingle'
47− , putObjectSingle
48− , copyObjectSingle
49−
50− -- * Multipart Upload APIs
51− --------------------------
52− , UploadId
53− , PartTuple
54− , Payload(..)
55− , PartNumber
56− , newMultipartUpload
57− , putObjectPart
58− , copyObjectPart
59− , completeMultipartUpload
60− , abortMultipartUpload
61− , ListUploadsResult(..)
62− , listIncompleteUploads'
63− , ListPartsResult(..)
64− , listIncompleteParts'
65−
66− -- * Deletion APIs
67− --------------------------
68− , deleteBucket
69− , deleteObject
70−
71− -- * Presigned Operations
72− -----------------------------
73− , module Network.Minio.PresignedOperations
74−
75− -- ** Bucket Policies
76− , getBucketPolicy
77− , setBucketPolicy
78−
79− -- * Bucket Notifications
80− -------------------------
81− , Notification(..)
82− , NotificationConfig(..)
83− , Arn
84− , Event(..)
85− , Filter(..)
86− , FilterKey(..)
87− , FilterRules(..)
88− , FilterRule(..)
89− , getBucketNotification
90− , putBucketNotification
91− , removeAllBucketNotification
92− ) where
93−
94−import qualified Data.ByteString as BS
95−import qualified Data.Text as T
96−import qualified Network.HTTP.Conduit as NC
97−import qualified Network.HTTP.Types as HT
98−import Network.HTTP.Types.Status (status404)
99−import UnliftIO (Handler (Handler))
100−
101−import Lib.Prelude
102−
103−import Network.Minio.API
104−import Network.Minio.APICommon
105−import Network.Minio.Data
106−import Network.Minio.Errors
107−import Network.Minio.PresignedOperations
108−import Network.Minio.Utils
109−import Network.Minio.XmlGenerator
110−import Network.Minio.XmlParser
18+ ( Region,
19+ getLocation,
20+
21+ -- * Listing buckets
22+ --------------------
23+ getService,
24+
25+ -- * Listing objects
26+ --------------------
27+ ListObjectsResult (..),
28+ ListObjectsV1Result (..),
29+ listObjects',
30+ listObjectsV1',
31+
32+ -- * Retrieving buckets
33+ headBucket,
34+
35+ -- * Retrieving objects
36+ -----------------------
37+ getObject',
38+ headObject,
39+
40+ -- * Creating buckets and objects
41+ ---------------------------------
42+ putBucket,
43+ ETag,
44+ maxSinglePutObjectSizeBytes,
45+ putObjectSingle',
46+ putObjectSingle,
47+ copyObjectSingle,
48+
49+ -- * Multipart Upload APIs
50+ --------------------------
51+ UploadId,
52+ PartTuple,
53+ Payload (..),
54+ PartNumber,
55+ newMultipartUpload,
56+ putObjectPart,
57+ copyObjectPart,
58+ completeMultipartUpload,
59+ abortMultipartUpload,
60+ ListUploadsResult (..),
61+ listIncompleteUploads',
62+ ListPartsResult (..),
63+ listIncompleteParts',
64+
65+ -- * Deletion APIs
66+ --------------------------
67+ deleteBucket,
68+ deleteObject,
69+
70+ -- * Presigned Operations
71+ -----------------------------
72+ module Network.Minio.PresignedOperations,
73+
74+ -- ** Bucket Policies
75+ getBucketPolicy,
76+ setBucketPolicy,
77+
78+ -- * Bucket Notifications
79+ -------------------------
80+ Notification (..),
81+ NotificationConfig (..),
82+ Arn,
83+ Event (..),
84+ Filter (..),
85+ FilterKey (..),
86+ FilterRules (..),
87+ FilterRule (..),
88+ getBucketNotification,
89+ putBucketNotification,
90+ removeAllBucketNotification,
91+ )
92+where
93+
94+import qualified Data.ByteString as BS
95+import qualified Data.Text as T
96+import Lib.Prelude
97+import qualified Network.HTTP.Conduit as NC
98+import qualified Network.HTTP.Types as HT
99+import Network.HTTP.Types.Status (status404)
100+import Network.Minio.API
101+import Network.Minio.APICommon
102+import Network.Minio.Data
103+import Network.Minio.Errors
104+import Network.Minio.PresignedOperations
105+import Network.Minio.Utils
106+import Network.Minio.XmlGenerator
107+import Network.Minio.XmlParser
108+import UnliftIO (Handler (Handler))
111109
112110-- | Fetch all buckets from the service.
113111getService :: Minio [BucketInfo]
114112getService = do
115− resp <- executeRequest $ defaultS3ReqInfo {
116− riNeedsLocation = False
117− }
113+ resp <-
114+ executeRequest $
115+ defaultS3ReqInfo
116+ { riNeedsLocation = False
117+ }
118118 parseListBuckets $ NC.responseBody resp
119119
120120-- Parse headers from getObject and headObject calls.
121121parseGetObjectHeaders :: Object -> [HT.Header] -> Maybe ObjectInfo
122122parseGetObjectHeaders object headers =
123− let metadataPairs = getMetadata headers
124− userMetadata = getUserMetadataMap metadataPairs
125− metadata = getNonUserMetadataMap metadataPairs
126− in ObjectInfo <$> Just object
127− <*> getLastModifiedHeader headers
128− <*> getETagHeader headers
129− <*> getContentLength headers
130− <*> Just userMetadata
131− <*> Just metadata
123+ let metadataPairs = getMetadata headers
124+ userMetadata = getUserMetadataMap metadataPairs
125+ metadata = getNonUserMetadataMap metadataPairs
126+ in ObjectInfo <$> Just object
127+ <*> getLastModifiedHeader headers
128+ <*> getETagHeader headers
129+ <*> getContentLength headers
130+ <*> Just userMetadata
131+ <*> Just metadata
132132
133133-- | GET an object from the service and return parsed ObjectInfo and a
134134-- conduit source for the object content
135−getObject' :: Bucket -> Object -> HT.Query -> [HT.Header]
136− -> Minio GetObjectResponse
135+getObject' ::
136+ Bucket ->
137+ Object ->
138+ HT.Query ->
139+ [HT.Header] ->
140+ Minio GetObjectResponse
137141getObject' bucket object queryParams headers = do
138− resp <- mkStreamRequest reqInfo
139− let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
140− objInfo <- maybe (throwIO MErrVInvalidObjectInfoResponse) return
141− objInfoMaybe
142− return $ GetObjectResponse { gorObjectInfo = objInfo
143− , gorObjectStream = NC.responseBody resp
144− }
142+ resp <- mkStreamRequest reqInfo
143+ let objInfoMaybe = parseGetObjectHeaders object $ NC.responseHeaders resp
144+ objInfo <-
145+ maybe
146+ (throwIO MErrVInvalidObjectInfoResponse)
147+ return
148+ objInfoMaybe
149+ return $
150+ GetObjectResponse
151+ { gorObjectInfo = objInfo,
152+ gorObjectStream = NC.responseBody resp
153+ }
145154 where
146− reqInfo = defaultS3ReqInfo { riBucket = Just bucket
147− , riObject = Just object
148− , riQueryParams = queryParams
149− , riHeaders = headers
150− }
155+ reqInfo =
156+ defaultS3ReqInfo
157+ { riBucket = Just bucket,
158+ riObject = Just object,
159+ riQueryParams = queryParams,
160+ riHeaders = headers
161+ }
151162
152163-- | Creates a bucket via a PUT bucket call.
153164putBucket :: Bucket -> Region -> Minio ()
154165putBucket bucket location = do
155166 ns <- asks getSvcNamespace
156167 void $ executeRequest $
157− defaultS3ReqInfo { riMethod = HT.methodPut
158− , riBucket = Just bucket
159− , riPayload = PayloadBS $ mkCreateBucketConfig ns location
160− , riNeedsLocation = False
161− }
168+ defaultS3ReqInfo
169+ { riMethod = HT.methodPut,
170+ riBucket = Just bucket,
171+ riPayload = PayloadBS $ mkCreateBucketConfig ns location,
172+ riNeedsLocation = False
173+ }
162174
163175-- | Single PUT object size.
164176maxSinglePutObjectSizeBytes :: Int64
@@ -172,315 +184,429 @@ putObjectSingle' :: Bucket -> Object -> [HT.Header] -> ByteString -> Minio ETag
172184putObjectSingle' bucket object headers bs = do
173185 let size = fromIntegral (BS.length bs)
174186 -- check length is within single PUT object size.
175− when (size > maxSinglePutObjectSizeBytes) $
176− throwIO $ MErrVSinglePUTSizeExceeded size
187+ when (size > maxSinglePutObjectSizeBytes)
188+ $ throwIO
189+ $ MErrVSinglePUTSizeExceeded size
177190
178191 let payload = mkStreamingPayload $ PayloadBS bs
179− resp <- executeRequest $
180− defaultS3ReqInfo { riMethod = HT.methodPut
181− , riBucket = Just bucket
182− , riObject = Just object
183− , riHeaders = headers
184− , riPayload = payload
185− }
192+ resp <-
193+ executeRequest $
194+ defaultS3ReqInfo
195+ { riMethod = HT.methodPut,
196+ riBucket = Just bucket,
197+ riObject = Just object,
198+ riHeaders = headers,
199+ riPayload = payload
200+ }
186201
187202 let rheaders = NC.responseHeaders resp
188203 etag = getETagHeader rheaders
189204 maybe
190205 (throwIO MErrVETagHeaderNotFound)
191− return etag
206+ return
207+ etag
192208
193209-- | PUT an object into the service. This function performs a single
194210-- PUT object call, and so can only transfer objects upto 5GiB.
195−putObjectSingle :: Bucket -> Object -> [HT.Header] -> Handle -> Int64
196− -> Int64 -> Minio ETag
211+putObjectSingle ::
212+ Bucket ->
213+ Object ->
214+ [HT.Header] ->
215+ Handle ->
216+ Int64 ->
217+ Int64 ->
218+ Minio ETag
197219putObjectSingle bucket object headers h offset size = do
198220 -- check length is within single PUT object size.
199− when (size > maxSinglePutObjectSizeBytes) $
200− throwIO $ MErrVSinglePUTSizeExceeded size
221+ when (size > maxSinglePutObjectSizeBytes)
222+ $ throwIO
223+ $ MErrVSinglePUTSizeExceeded size
201224
202225 -- content-length header is automatically set by library.
203226 let payload = mkStreamingPayload $ PayloadH h offset size
204− resp <- executeRequest $
205− defaultS3ReqInfo { riMethod = HT.methodPut
206− , riBucket = Just bucket
207− , riObject = Just object
208− , riHeaders = headers
209− , riPayload = payload
210− }
227+ resp <-
228+ executeRequest $
229+ defaultS3ReqInfo
230+ { riMethod = HT.methodPut,
231+ riBucket = Just bucket,
232+ riObject = Just object,
233+ riHeaders = headers,
234+ riPayload = payload
235+ }
211236
212237 let rheaders = NC.responseHeaders resp
213238 etag = getETagHeader rheaders
214239 maybe
215240 (throwIO MErrVETagHeaderNotFound)
216− return etag
241+ return
242+ etag
217243
218244-- | List objects in a bucket matching prefix up to delimiter,
219245-- starting from nextMarker.
220−listObjectsV1' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
221− -> Minio ListObjectsV1Result
246+listObjectsV1' ::
247+ Bucket ->
248+ Maybe Text ->
249+ Maybe Text ->
250+ Maybe Text ->
251+ Maybe Int ->
252+ Minio ListObjectsV1Result
222253listObjectsV1' bucket prefix nextMarker delimiter maxKeys = do
223− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
224− , riBucket = Just bucket
225− , riQueryParams = mkOptionalParams params
226− }
254+ resp <-
255+ executeRequest $
256+ defaultS3ReqInfo
257+ { riMethod = HT.methodGet,
258+ riBucket = Just bucket,
259+ riQueryParams = mkOptionalParams params
260+ }
227261 parseListObjectsV1Response $ NC.responseBody resp
228262 where
229− params = [
230− ("marker", nextMarker)
231− , ("prefix", prefix)
232− , ("delimiter", delimiter)
233− , ("max-keys", show <$> maxKeys)
263+ params =
264+ [ ("marker", nextMarker),
265+ ("prefix", prefix),
266+ ("delimiter", delimiter),
267+ ("max-keys", show <$> maxKeys)
234268 ]
235269
236270-- | List objects in a bucket matching prefix up to delimiter,
237271-- starting from nextToken.
238−listObjects' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text -> Maybe Int
239− -> Minio ListObjectsResult
272+listObjects' ::
273+ Bucket ->
274+ Maybe Text ->
275+ Maybe Text ->
276+ Maybe Text ->
277+ Maybe Int ->
278+ Minio ListObjectsResult
240279listObjects' bucket prefix nextToken delimiter maxKeys = do
241− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
242− , riBucket = Just bucket
243− , riQueryParams = mkOptionalParams params
244− }
280+ resp <-
281+ executeRequest $
282+ defaultS3ReqInfo
283+ { riMethod = HT.methodGet,
284+ riBucket = Just bucket,
285+ riQueryParams = mkOptionalParams params
286+ }
245287 parseListObjectsResponse $ NC.responseBody resp
246288 where
247− params = [
248− ("list-type", Just "2")
249− , ("continuation_token", nextToken)
250− , ("prefix", prefix)
251− , ("delimiter", delimiter)
252− , ("max-keys", show <$> maxKeys)
289+ params =
290+ [ ("list-type", Just "2"),
291+ ("continuation_token", nextToken),
292+ ("prefix", prefix),
293+ ("delimiter", delimiter),
294+ ("max-keys", show <$> maxKeys)
253295 ]
254296
255297-- | DELETE a bucket from the service.
256298deleteBucket :: Bucket -> Minio ()
257−deleteBucket bucket = void $
258− executeRequest $
259− defaultS3ReqInfo { riMethod = HT.methodDelete
260− , riBucket = Just bucket
261− }
299+deleteBucket bucket =
300+ void
301+ $ executeRequest
302+ $ defaultS3ReqInfo
303+ { riMethod = HT.methodDelete,
304+ riBucket = Just bucket
305+ }
262306
263307-- | DELETE an object from the service.
264308deleteObject :: Bucket -> Object -> Minio ()
265−deleteObject bucket object = void $
266− executeRequest $
267− defaultS3ReqInfo { riMethod = HT.methodDelete
268− , riBucket = Just bucket
269− , riObject = Just object
270− }
309+deleteObject bucket object =
310+ void
311+ $ executeRequest
312+ $ defaultS3ReqInfo
313+ { riMethod = HT.methodDelete,
314+ riBucket = Just bucket,
315+ riObject = Just object
316+ }
271317
272318-- | Create a new multipart upload.
273319newMultipartUpload :: Bucket -> Object -> [HT.Header] -> Minio UploadId
274320newMultipartUpload bucket object headers = do
275− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPost
276− , riBucket = Just bucket
277− , riObject = Just object
278− , riQueryParams = [("uploads", Nothing)]
279− , riHeaders = headers
280− }
321+ resp <-
322+ executeRequest $
323+ defaultS3ReqInfo
324+ { riMethod = HT.methodPost,
325+ riBucket = Just bucket,
326+ riObject = Just object,
327+ riQueryParams = [("uploads", Nothing)],
328+ riHeaders = headers
329+ }
281330 parseNewMultipartUpload $ NC.responseBody resp
282331
283332-- | PUT a part of an object as part of a multipart upload.
284−putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
285− -> Payload -> Minio PartTuple
333+putObjectPart ::
334+ Bucket ->
335+ Object ->
336+ UploadId ->
337+ PartNumber ->
338+ [HT.Header] ->
339+ Payload ->
340+ Minio PartTuple
286341putObjectPart bucket object uploadId partNumber headers payload = do
287342 -- transform payload to conduit to enable streaming signature
288343 let payload' = mkStreamingPayload payload
289− resp <- executeRequest $
290− defaultS3ReqInfo { riMethod = HT.methodPut
291− , riBucket = Just bucket
292− , riObject = Just object
293− , riQueryParams = mkOptionalParams params
294− , riHeaders = headers
295− , riPayload = payload'
296− }
344+ resp <-
345+ executeRequest $
346+ defaultS3ReqInfo
347+ { riMethod = HT.methodPut,
348+ riBucket = Just bucket,
349+ riObject = Just object,
350+ riQueryParams = mkOptionalParams params,
351+ riHeaders = headers,
352+ riPayload = payload'
353+ }
297354 let rheaders = NC.responseHeaders resp
298355 etag = getETagHeader rheaders
299356 maybe
300357 (throwIO MErrVETagHeaderNotFound)
301− (return . (partNumber, )) etag
358+ (return . (partNumber,))
359+ etag
302360 where
303− params = [
304− ("uploadId", Just uploadId)
305− , ("partNumber", Just $ show partNumber)
361+ params =
362+ [ ("uploadId", Just uploadId),
363+ ("partNumber", Just $ show partNumber)
306364 ]
307365
308366srcInfoToHeaders :: SourceInfo -> [HT.Header]
309−srcInfoToHeaders srcInfo = ("x-amz-copy-source",
310− toS $ T.concat ["/", srcBucket srcInfo,
311− "/", srcObject srcInfo]
312− ) : rangeHdr ++ zip names values
367+srcInfoToHeaders srcInfo =
368+ ( "x-amz-copy-source",
369+ toUtf8 $
370+ T.concat
371+ [ "/",
372+ srcBucket srcInfo,
373+ "/",
374+ srcObject srcInfo
375+ ]
376+ )
377+ : rangeHdr
378+ ++ zip names values
313379 where
314− names = ["x-amz-copy-source-if-match", "x-amz-copy-source-if-none-match",
315− "x-amz-copy-source-if-unmodified-since",
316− "x-amz-copy-source-if-modified-since"]
317− values = mapMaybe (fmap encodeUtf8 . (srcInfo &))
318− [srcIfMatch, srcIfNoneMatch,
319− fmap formatRFC1123 . srcIfUnmodifiedSince,
320− fmap formatRFC1123 . srcIfModifiedSince]
321− rangeHdr = maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])])
322− $ toByteRange <$> srcRange srcInfo
380+ names =
381+ [ "x-amz-copy-source-if-match",
382+ "x-amz-copy-source-if-none-match",
383+ "x-amz-copy-source-if-unmodified-since",
384+ "x-amz-copy-source-if-modified-since"
385+ ]
386+ values =
387+ mapMaybe
388+ (fmap encodeUtf8 . (srcInfo &))
389+ [ srcIfMatch,
390+ srcIfNoneMatch,
391+ fmap formatRFC1123 . srcIfUnmodifiedSince,
392+ fmap formatRFC1123 . srcIfModifiedSince
393+ ]
394+ rangeHdr =
395+ maybe [] (\a -> [("x-amz-copy-source-range", HT.renderByteRanges [a])]) $
396+ toByteRange <$> srcRange srcInfo
323397 toByteRange :: (Int64, Int64) -> HT.ByteRange
324398 toByteRange (x, y) = HT.ByteRangeFromTo (fromIntegral x) (fromIntegral y)
325399
326400-- | Performs server-side copy of an object or part of an object as an
327401-- upload part of an ongoing multi-part upload.
328−copyObjectPart :: DestinationInfo -> SourceInfo -> UploadId
329− -> PartNumber -> [HT.Header] -> Minio (ETag, UTCTime)
402+copyObjectPart ::
403+ DestinationInfo ->
404+ SourceInfo ->
405+ UploadId ->
406+ PartNumber ->
407+ [HT.Header] ->
408+ Minio (ETag, UTCTime)
330409copyObjectPart dstInfo srcInfo uploadId partNumber headers = do
331− resp <- executeRequest $
332− defaultS3ReqInfo { riMethod = HT.methodPut
333− , riBucket = Just $ dstBucket dstInfo
334− , riObject = Just $ dstObject dstInfo
335− , riQueryParams = mkOptionalParams params
336− , riHeaders = headers ++ srcInfoToHeaders srcInfo
337− }
410+ resp <-
411+ executeRequest $
412+ defaultS3ReqInfo
413+ { riMethod = HT.methodPut,
414+ riBucket = Just $ dstBucket dstInfo,
415+ riObject = Just $ dstObject dstInfo,
416+ riQueryParams = mkOptionalParams params,
417+ riHeaders = headers ++ srcInfoToHeaders srcInfo
418+ }
338419
339420 parseCopyObjectResponse $ NC.responseBody resp
340421 where
341− params = [
342− ("uploadId", Just uploadId)
343− , ("partNumber", Just $ show partNumber)
422+ params =
423+ [ ("uploadId", Just uploadId),
424+ ("partNumber", Just $ show partNumber)
344425 ]
345426
346427-- | Performs server-side copy of an object that is upto 5GiB in
347428-- size. If the object is greater than 5GiB, this function throws the
348429-- error returned by the server.
349−copyObjectSingle :: Bucket -> Object -> SourceInfo -> [HT.Header]
350− -> Minio (ETag, UTCTime)
430+copyObjectSingle ::
431+ Bucket ->
432+ Object ->
433+ SourceInfo ->
434+ [HT.Header] ->
435+ Minio (ETag, UTCTime)
351436copyObjectSingle bucket object srcInfo headers = do
352437 -- validate that srcRange is Nothing for this API.
353438 when (isJust $ srcRange srcInfo) $
354439 throwIO MErrVCopyObjSingleNoRangeAccepted
355− resp <- executeRequest $
356− defaultS3ReqInfo { riMethod = HT.methodPut
357− , riBucket = Just bucket
358− , riObject = Just object
359− , riHeaders = headers ++ srcInfoToHeaders srcInfo
360− }
440+ resp <-
441+ executeRequest $
442+ defaultS3ReqInfo
443+ { riMethod = HT.methodPut,
444+ riBucket = Just bucket,
445+ riObject = Just object,
446+ riHeaders = headers ++ srcInfoToHeaders srcInfo
447+ }
361448 parseCopyObjectResponse $ NC.responseBody resp
362449
363450-- | Complete a multipart upload.
364−completeMultipartUpload :: Bucket -> Object -> UploadId -> [PartTuple]
365− -> Minio ETag
451+completeMultipartUpload ::
452+ Bucket ->
453+ Object ->
454+ UploadId ->
455+ [PartTuple] ->
456+ Minio ETag
366457completeMultipartUpload bucket object uploadId partTuple = do
367− resp <- executeRequest $
368− defaultS3ReqInfo { riMethod = HT.methodPost
369− , riBucket = Just bucket
370− , riObject = Just object
371− , riQueryParams = mkOptionalParams params
372− , riPayload = PayloadBS $
373− mkCompleteMultipartUploadRequest partTuple
374− }
458+ resp <-
459+ executeRequest $
460+ defaultS3ReqInfo
461+ { riMethod = HT.methodPost,
462+ riBucket = Just bucket,
463+ riObject = Just object,
464+ riQueryParams = mkOptionalParams params,
465+ riPayload =
466+ PayloadBS $
467+ mkCompleteMultipartUploadRequest partTuple
468+ }
375469 parseCompleteMultipartUploadResponse $ NC.responseBody resp
376470 where
377471 params = [("uploadId", Just uploadId)]
378472
379473-- | Abort a multipart upload.
380474abortMultipartUpload :: Bucket -> Object -> UploadId -> Minio ()
381−abortMultipartUpload bucket object uploadId = void $
382− executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
383− , riBucket = Just bucket
384− , riObject = Just object
385− , riQueryParams = mkOptionalParams params
386− }
475+abortMultipartUpload bucket object uploadId =
476+ void
477+ $ executeRequest
478+ $ defaultS3ReqInfo
479+ { riMethod = HT.methodDelete,
480+ riBucket = Just bucket,
481+ riObject = Just object,
482+ riQueryParams = mkOptionalParams params
483+ }
387484 where
388485 params = [("uploadId", Just uploadId)]
389486
390487-- | List incomplete multipart uploads.
391−listIncompleteUploads' :: Bucket -> Maybe Text -> Maybe Text -> Maybe Text
392− -> Maybe Text -> Maybe Int -> Minio ListUploadsResult
488+listIncompleteUploads' ::
489+ Bucket ->
490+ Maybe Text ->
491+ Maybe Text ->
492+ Maybe Text ->
493+ Maybe Text ->
494+ Maybe Int ->
495+ Minio ListUploadsResult
393496listIncompleteUploads' bucket prefix delimiter keyMarker uploadIdMarker maxKeys = do
394− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
395− , riBucket = Just bucket
396− , riQueryParams = params
397− }
497+ resp <-
498+ executeRequest $
499+ defaultS3ReqInfo
500+ { riMethod = HT.methodGet,
501+ riBucket = Just bucket,
502+ riQueryParams = params
503+ }
398504 parseListUploadsResponse $ NC.responseBody resp
399505 where
400506 -- build query params
401− params = ("uploads", Nothing) : mkOptionalParams
402− [ ("prefix", prefix)
403− , ("delimiter", delimiter)
404− , ("key-marker", keyMarker)
405− , ("upload-id-marker", uploadIdMarker)
406− , ("max-uploads", show <$> maxKeys)
407− ]
408−
507+ params =
508+ ("uploads", Nothing)
509+ : mkOptionalParams
510+ [ ("prefix", prefix),
511+ ("delimiter", delimiter),
512+ ("key-marker", keyMarker),
513+ ("upload-id-marker", uploadIdMarker),
514+ ("max-uploads", show <$> maxKeys)
515+ ]
409516
410517-- | List parts of an ongoing multipart upload.
411−listIncompleteParts' :: Bucket -> Object -> UploadId -> Maybe Text
412− -> Maybe Text -> Minio ListPartsResult
518+listIncompleteParts' ::
519+ Bucket ->
520+ Object ->
521+ UploadId ->
522+ Maybe Text ->
523+ Maybe Text ->
524+ Minio ListPartsResult
413525listIncompleteParts' bucket object uploadId maxParts partNumMarker = do
414− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
415− , riBucket = Just bucket
416− , riObject = Just object
417− , riQueryParams = mkOptionalParams params
418− }
526+ resp <-
527+ executeRequest $
528+ defaultS3ReqInfo
529+ { riMethod = HT.methodGet,
530+ riBucket = Just bucket,
531+ riObject = Just object,
532+ riQueryParams = mkOptionalParams params
533+ }
419534 parseListPartsResponse $ NC.responseBody resp
420535 where
421536 -- build optional query params
422− params = [
423− ("uploadId", Just uploadId)
424− , ("part-number-marker", partNumMarker)
425− , ("max-parts", maxParts)
537+ params =
538+ [ ("uploadId", Just uploadId),
539+ ("part-number-marker", partNumMarker),
540+ ("max-parts", maxParts)
426541 ]
427542
428543-- | Get metadata of an object.
429544headObject :: Bucket -> Object -> [HT.Header] -> Minio ObjectInfo
430545headObject bucket object reqHeaders = do
431− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
432− , riBucket = Just bucket
433− , riObject = Just object
434− , riHeaders = reqHeaders
435− }
436−
437− maybe (throwIO MErrVInvalidObjectInfoResponse) return $
438− parseGetObjectHeaders object $ NC.responseHeaders resp
546+ resp <-
547+ executeRequest $
548+ defaultS3ReqInfo
549+ { riMethod = HT.methodHead,
550+ riBucket = Just bucket,
551+ riObject = Just object,
552+ riHeaders = reqHeaders
553+ }
439554
555+ maybe (throwIO MErrVInvalidObjectInfoResponse) return
556+ $ parseGetObjectHeaders object
557+ $ NC.responseHeaders resp
440558
441559-- | Query the object store if a given bucket exists.
442560headBucket :: Bucket -> Minio Bool
443−headBucket bucket = headBucketEx `catches`
444− [ Handler handleNoSuchBucket
445− , Handler handleStatus404
446− ]
447−
561+headBucket bucket =
562+ headBucketEx
563+ `catches` [ Handler handleNoSuchBucket,
564+ Handler handleStatus404
565+ ]
448566 where
449567 handleNoSuchBucket :: ServiceErr -> Minio Bool
450− handleNoSuchBucket e | e == NoSuchBucket = return False
451− | otherwise = throwIO e
452−
568+ handleNoSuchBucket e
569+ | e == NoSuchBucket = return False
570+ | otherwise = throwIO e
453571 handleStatus404 :: NC.HttpException -> Minio Bool
454572 handleStatus404 e@(NC.HttpExceptionRequest _ (NC.StatusCodeException res _)) =
455573 if NC.responseStatus res == status404
456− then return False
457− else throwIO e
574+ then return False
575+ else throwIO e
458576 handleStatus404 e = throwIO e
459−
460577 headBucketEx = do
461− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodHead
462− , riBucket = Just bucket
463− }
578+ resp <-
579+ executeRequest $
580+ defaultS3ReqInfo
581+ { riMethod = HT.methodHead,
582+ riBucket = Just bucket
583+ }
464584 return $ NC.responseStatus resp == HT.ok200
465585
466586-- | Set the notification configuration on a bucket.
467587putBucketNotification :: Bucket -> Notification -> Minio ()
468588putBucketNotification bucket ncfg = do
469589 ns <- asks getSvcNamespace
470− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
471− , riBucket = Just bucket
472− , riQueryParams = [("notification", Nothing)]
473− , riPayload = PayloadBS $
474− mkPutNotificationRequest ns ncfg
475− }
590+ void $ executeRequest $
591+ defaultS3ReqInfo
592+ { riMethod = HT.methodPut,
593+ riBucket = Just bucket,
594+ riQueryParams = [("notification", Nothing)],
595+ riPayload =
596+ PayloadBS $
597+ mkPutNotificationRequest ns ncfg
598+ }
476599
477600-- | Retrieve the notification configuration on a bucket.
478601getBucketNotification :: Bucket -> Minio Notification
479602getBucketNotification bucket = do
480− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
481− , riBucket = Just bucket
482− , riQueryParams = [("notification", Nothing)]
483− }
603+ resp <-
604+ executeRequest $
605+ defaultS3ReqInfo
606+ { riMethod = HT.methodGet,
607+ riBucket = Just bucket,
608+ riQueryParams = [("notification", Nothing)]
609+ }
484610 parseNotification $ NC.responseBody resp
485611
486612-- | Remove all notifications configured on a bucket.
@@ -490,11 +616,14 @@ removeAllBucketNotification = flip putBucketNotification defaultNotification
490616-- | Fetch the policy if any on a bucket.
491617getBucketPolicy :: Bucket -> Minio Text
492618getBucketPolicy bucket = do
493− resp <- executeRequest $ defaultS3ReqInfo { riMethod = HT.methodGet
494− , riBucket = Just bucket
495− , riQueryParams = [("policy", Nothing)]
496− }
497− return $ toS $ NC.responseBody resp
619+ resp <-
620+ executeRequest $
621+ defaultS3ReqInfo
622+ { riMethod = HT.methodGet,
623+ riBucket = Just bucket,
624+ riQueryParams = [("policy", Nothing)]
625+ }
626+ return $ decodeUtf8Lenient $ toStrictBS $ NC.responseBody resp
498627
499628-- | Set a new policy on a bucket.
500629-- As a special condition if the policy is empty
@@ -506,18 +635,22 @@ setBucketPolicy bucket policy = do
506635 else putBucketPolicy bucket policy
507636
508637-- | Save a new policy on a bucket.
509−putBucketPolicy :: Bucket -> Text -> Minio()
638+putBucketPolicy :: Bucket -> Text -> Minio ()
510639putBucketPolicy bucket policy = do
511− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodPut
512− , riBucket = Just bucket
513− , riQueryParams = [("policy", Nothing)]
514− , riPayload = PayloadBS $ encodeUtf8 policy
515− }
640+ void $ executeRequest $
641+ defaultS3ReqInfo
642+ { riMethod = HT.methodPut,
643+ riBucket = Just bucket,
644+ riQueryParams = [("policy", Nothing)],
645+ riPayload = PayloadBS $ encodeUtf8 policy
646+ }
516647
517648-- | Delete any policy set on a bucket.
518−deleteBucketPolicy :: Bucket -> Minio()
649+deleteBucketPolicy :: Bucket -> Minio ()
519650deleteBucketPolicy bucket = do
520− void $ executeRequest $ defaultS3ReqInfo { riMethod = HT.methodDelete
521− , riBucket = Just bucket
522− , riQueryParams = [("policy", Nothing)]
523− }
651+ void $ executeRequest $
652+ defaultS3ReqInfo
653+ { riMethod = HT.methodDelete,
654+ riBucket = Just bucket,
655+ riQueryParams = [("policy", Nothing)]
656+ }
src/Network/Minio/SelectAPI.hs+228−233
@@ -15,113 +15,103 @@
1515--
1616
1717module Network.Minio.SelectAPI
18− (
19−
20− -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
21− -- format objects in AWS S3 and in MinIO using SQL Select
22− -- statements. This allows significant reduction of data transfer
23− -- from object storage for computation-intensive tasks, as relevant
24− -- data is filtered close to the storage.
25−
26− selectObjectContent
27−
28− , SelectRequest
29− , selectRequest
30−
31− -- *** Input Serialization
32−
33− , InputSerialization
34− , defaultCsvInput
35− , linesJsonInput
36− , documentJsonInput
37− , defaultParquetInput
38− , setInputCSVProps
39−
40− , CompressionType(..)
41− , setInputCompressionType
42−
43− -- *** CSV Format details
44−
45− -- | CSV format options such as delimiters and quote characters are
46− -- specified using using the functions below. Options are combined
47− -- monoidally.
48−
49− , CSVProp
50− , recordDelimiter
51− , fieldDelimiter
52− , quoteCharacter
53− , quoteEscapeCharacter
54− , commentCharacter
55− , allowQuotedRecordDelimiter
56− , FileHeaderInfo(..)
57− , fileHeaderInfo
58− , QuoteFields(..)
59− , quoteFields
60−
61− -- *** Output Serialization
62−
63− , OutputSerialization
64− , defaultCsvOutput
65− , defaultJsonOutput
66− , outputCSVFromProps
67− , outputJSONFromRecordDelimiter
68−
69− -- *** Progress messages
70−
71− , setRequestProgressEnabled
72−
73− -- *** Interpreting Select output
74−
75− -- | The conduit returned by `selectObjectContent` returns values of
76− -- the `EventMessage` data type. This returns the query output
77− -- messages formatted according to the chosen output serialization,
78− -- interleaved with progress messages (if enabled by
79− -- `setRequestProgressEnabled`), and at the end a statistics
80− -- message.
81− --
82− -- If the application is interested in only the payload, then
83− -- `getPayloadBytes` can be used. For example to simply print the
84− -- payload to stdout:
85− --
86− -- > resultConduit <- selectObjectContent bucket object mySelectRequest
87− -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
88− --
89− -- Note that runConduit, the connect operator (.|) and stdoutC are
90− -- all from the "conduit" package.
91−
92− , getPayloadBytes
93− , EventMessage(..)
94− , Progress(..)
95− , Stats
96− ) where
97−
98−import Conduit ((.|))
99−import qualified Conduit as C
100−import qualified Data.Binary as Bin
101−import qualified Data.ByteString as B
102−import qualified Data.ByteString.Lazy as LB
103−import Data.Digest.CRC32 (crc32, crc32Update)
104−import qualified Network.HTTP.Conduit as NC
105−import qualified Network.HTTP.Types as HT
106−import UnliftIO (MonadUnliftIO)
107−
108−import Lib.Prelude
109−
110−import Network.Minio.API
111−import Network.Minio.Data
112−import Network.Minio.Errors
113−import Network.Minio.Utils
114−import Network.Minio.XmlGenerator
115−import Network.Minio.XmlParser
116−
117−data EventStreamException = ESEPreludeCRCFailed
118− | ESEMessageCRCFailed
119− | ESEUnexpectedEndOfStream
120− | ESEDecodeFail [Char]
121− | ESEInvalidHeaderType
122− | ESEInvalidHeaderValueType
123− | ESEInvalidMessageType
124− deriving (Eq, Show)
18+ ( -- | The `selectObjectContent` allows querying CSV, JSON or Parquet
19+ -- format objects in AWS S3 and in MinIO using SQL Select
20+ -- statements. This allows significant reduction of data transfer
21+ -- from object storage for computation-intensive tasks, as relevant
22+ -- data is filtered close to the storage.
23+ selectObjectContent,
24+ SelectRequest,
25+ selectRequest,
26+
27+ -- *** Input Serialization
28+ InputSerialization,
29+ defaultCsvInput,
30+ linesJsonInput,
31+ documentJsonInput,
32+ defaultParquetInput,
33+ setInputCSVProps,
34+ CompressionType (..),
35+ setInputCompressionType,
36+
37+ -- *** CSV Format details
38+
39+ -- | CSV format options such as delimiters and quote characters are
40+ -- specified using using the functions below. Options are combined
41+ -- monoidally.
42+ CSVProp,
43+ recordDelimiter,
44+ fieldDelimiter,
45+ quoteCharacter,
46+ quoteEscapeCharacter,
47+ commentCharacter,
48+ allowQuotedRecordDelimiter,
49+ FileHeaderInfo (..),
50+ fileHeaderInfo,
51+ QuoteFields (..),
52+ quoteFields,
53+
54+ -- *** Output Serialization
55+ OutputSerialization,
56+ defaultCsvOutput,
57+ defaultJsonOutput,
58+ outputCSVFromProps,
59+ outputJSONFromRecordDelimiter,
60+
61+ -- *** Progress messages
62+ setRequestProgressEnabled,
63+
64+ -- *** Interpreting Select output
65+
66+ -- | The conduit returned by `selectObjectContent` returns values of
67+ -- the `EventMessage` data type. This returns the query output
68+ -- messages formatted according to the chosen output serialization,
69+ -- interleaved with progress messages (if enabled by
70+ -- `setRequestProgressEnabled`), and at the end a statistics
71+ -- message.
72+ --
73+ -- If the application is interested in only the payload, then
74+ -- `getPayloadBytes` can be used. For example to simply print the
75+ -- payload to stdout:
76+ --
77+ -- > resultConduit <- selectObjectContent bucket object mySelectRequest
78+ -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
79+ --
80+ -- Note that runConduit, the connect operator (.|) and stdoutC are
81+ -- all from the "conduit" package.
82+ getPayloadBytes,
83+ EventMessage (..),
84+ Progress (..),
85+ Stats,
86+ )
87+where
88+
89+import Conduit ((.|))
90+import qualified Conduit as C
91+import qualified Data.Binary as Bin
92+import qualified Data.ByteString as B
93+import qualified Data.ByteString.Lazy as LB
94+import Data.Digest.CRC32 (crc32, crc32Update)
95+import Lib.Prelude
96+import qualified Network.HTTP.Conduit as NC
97+import qualified Network.HTTP.Types as HT
98+import Network.Minio.API
99+import Network.Minio.Data
100+import Network.Minio.Errors
101+import Network.Minio.Utils
102+import Network.Minio.XmlGenerator
103+import Network.Minio.XmlParser
104+import UnliftIO (MonadUnliftIO)
105+
106+data EventStreamException
107+ = ESEPreludeCRCFailed
108+ | ESEMessageCRCFailed
109+ | ESEUnexpectedEndOfStream
110+ | ESEDecodeFail [Char]
111+ | ESEInvalidHeaderType
112+ | ESEInvalidHeaderValueType
113+ | ESEInvalidMessageType
114+ deriving (Eq, Show)
125115
126116instance Exception EventStreamException
127117
@@ -131,169 +121,174 @@ chunkSize = 32 * 1024
131121
132122parseBinary :: Bin.Binary a => ByteString -> IO a
133123parseBinary b = do
134− case Bin.decodeOrFail $ LB.fromStrict b of
135− Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
136− Right (_, _, r) -> return r
124+ case Bin.decodeOrFail $ LB.fromStrict b of
125+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
126+ Right (_, _, r) -> return r
137127
138128bytesToHeaderName :: Text -> IO MsgHeaderName
139129bytesToHeaderName t = case t of
140− ":message-type" -> return MessageType
141− ":event-type" -> return EventType
142− ":content-type" -> return ContentType
143− ":error-code" -> return ErrorCode
130+ ":message-type" -> return MessageType
131+ ":event-type" -> return EventType
132+ ":content-type" -> return ContentType
133+ ":error-code" -> return ErrorCode
144134 ":error-message" -> return ErrorMessage
145− _ -> throwIO ESEInvalidHeaderType
135+ _ -> throwIO ESEInvalidHeaderType
146136
147−parseHeaders :: MonadUnliftIO m
148− => Word32 -> C.ConduitM ByteString a m [MessageHeader]
137+parseHeaders ::
138+ MonadUnliftIO m =>
139+ Word32 ->
140+ C.ConduitM ByteString a m [MessageHeader]
149141parseHeaders 0 = return []
150142parseHeaders hdrLen = do
151− bs1 <- readNBytes 1
152− n :: Word8 <- liftIO $ parseBinary bs1
143+ bs1 <- readNBytes 1
144+ n :: Word8 <- liftIO $ parseBinary bs1
153145
154− headerKeyBytes <- readNBytes $ fromIntegral n
155− let headerKey = decodeUtf8Lenient headerKeyBytes
156− headerName <- liftIO $ bytesToHeaderName headerKey
146+ headerKeyBytes <- readNBytes $ fromIntegral n
147+ let headerKey = decodeUtf8Lenient headerKeyBytes
148+ headerName <- liftIO $ bytesToHeaderName headerKey
157149
158− bs2 <- readNBytes 1
159− headerValueType :: Word8 <- liftIO $ parseBinary bs2
160− when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
150+ bs2 <- readNBytes 1
151+ headerValueType :: Word8 <- liftIO $ parseBinary bs2
152+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
161153
162− bs3 <- readNBytes 2
163− vLen :: Word16 <- liftIO $ parseBinary bs3
164− headerValueBytes <- readNBytes $ fromIntegral vLen
165− let headerValue = decodeUtf8Lenient headerValueBytes
166− m = (headerName, headerValue)
167− k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
154+ bs3 <- readNBytes 2
155+ vLen :: Word16 <- liftIO $ parseBinary bs3
156+ headerValueBytes <- readNBytes $ fromIntegral vLen
157+ let headerValue = decodeUtf8Lenient headerValueBytes
158+ m = (headerName, headerValue)
159+ k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
168160
169− ms <- parseHeaders (hdrLen - k)
170− return (m:ms)
161+ ms <- parseHeaders (hdrLen - k)
162+ return (m : ms)
171163
172164-- readNBytes returns N bytes read from the string and throws an
173165-- exception if N bytes are not present on the stream.
174166readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString
175167readNBytes n = do
176− b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
177− if B.length b /= n
178− then throwIO ESEUnexpectedEndOfStream
179− else return b
180−
181−crcCheck :: MonadUnliftIO m
182− => C.ConduitM ByteString ByteString m ()
168+ b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
169+ if B.length b /= n
170+ then throwIO ESEUnexpectedEndOfStream
171+ else return b
172+
173+crcCheck ::
174+ MonadUnliftIO m =>
175+ C.ConduitM ByteString ByteString m ()
183176crcCheck = do
184− b <- readNBytes 12
185− n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
186− preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
187− when (crc32 (B.take 8 b) /= preludeCRC) $
188− throwIO ESEPreludeCRCFailed
177+ b <- readNBytes 12
178+ n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
179+ preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
180+ when (crc32 (B.take 8 b) /= preludeCRC) $
181+ throwIO ESEPreludeCRCFailed
189182
190− -- we do not yield the checksum
191− C.yield $ B.take 8 b
183+ -- we do not yield the checksum
184+ C.yield $ B.take 8 b
192185
193− -- 12 bytes have been read off the current message. Now read the
194− -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
195− let startCrc = crc32 b
196− finalCrc <- accumulateYield (fromIntegral n-16) startCrc
186+ -- 12 bytes have been read off the current message. Now read the
187+ -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
188+ let startCrc = crc32 b
189+ finalCrc <- accumulateYield (fromIntegral n -16) startCrc
197190
198− bs <- readNBytes 4
199− expectedCrc :: Word32 <- liftIO $ parseBinary bs
191+ bs <- readNBytes 4
192+ expectedCrc :: Word32 <- liftIO $ parseBinary bs
200193
201− when (finalCrc /= expectedCrc) $
202− throwIO ESEMessageCRCFailed
194+ when (finalCrc /= expectedCrc) $
195+ throwIO ESEMessageCRCFailed
203196
204− -- we unconditionally recurse - downstream figures out when to
205− -- quit reading the stream
206− crcCheck
197+ -- we unconditionally recurse - downstream figures out when to
198+ -- quit reading the stream
199+ crcCheck
207200 where
208201 accumulateYield n checkSum = do
209− let toRead = min n chunkSize
210− b <- readNBytes toRead
211− let c' = crc32Update checkSum b
212− n' = n - B.length b
213− C.yield b
214− if n' > 0
215− then accumulateYield n' c'
216− else return c'
202+ let toRead = min n chunkSize
203+ b <- readNBytes toRead
204+ let c' = crc32Update checkSum b
205+ n' = n - B.length b
206+ C.yield b
207+ if n' > 0
208+ then accumulateYield n' c'
209+ else return c'
217210
218211handleMessage :: MonadUnliftIO m => C.ConduitT ByteString EventMessage m ()
219212handleMessage = do
220− b1 <- readNBytes 4
221− msgLen :: Word32 <- liftIO $ parseBinary b1
222−
223− b2 <- readNBytes 4
224− hdrLen :: Word32 <- liftIO $ parseBinary b2
225−
226− hs <- parseHeaders hdrLen
227−
228− let payloadLen = msgLen - hdrLen - 16
229− getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)
230− eventHdrValue = getHdrVal EventType hs
231− msgHdrValue = getHdrVal MessageType hs
232− errCode = getHdrVal ErrorCode hs
233− errMsg = getHdrVal ErrorMessage hs
234−
235− case msgHdrValue of
236− Just "event" -> do
237− case eventHdrValue of
238− Just "Records" -> passThrough $ fromIntegral payloadLen
239− Just "Cont" -> return ()
240− Just "Progress" -> do
241− bs <- readNBytes $ fromIntegral payloadLen
242− progress <- parseSelectProgress bs
243− C.yield $ ProgressEventMessage progress
244− Just "Stats" -> do
245− bs <- readNBytes $ fromIntegral payloadLen
246− stats <- parseSelectProgress bs
247− C.yield $ StatsEventMessage stats
248− Just "End" -> return ()
249− _ -> throwIO ESEInvalidMessageType
250− when (eventHdrValue /= Just "End") handleMessage
251−
252− Just "error" -> do
253− let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
254− maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
255−
256− _ -> throwIO ESEInvalidMessageType
257−
213+ b1 <- readNBytes 4
214+ msgLen :: Word32 <- liftIO $ parseBinary b1
215+
216+ b2 <- readNBytes 4
217+ hdrLen :: Word32 <- liftIO $ parseBinary b2
218+
219+ hs <- parseHeaders hdrLen
220+
221+ let payloadLen = msgLen - hdrLen - 16
222+ getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)
223+ eventHdrValue = getHdrVal EventType hs
224+ msgHdrValue = getHdrVal MessageType hs
225+ errCode = getHdrVal ErrorCode hs
226+ errMsg = getHdrVal ErrorMessage hs
227+
228+ case msgHdrValue of
229+ Just "event" -> do
230+ case eventHdrValue of
231+ Just "Records" -> passThrough $ fromIntegral payloadLen
232+ Just "Cont" -> return ()
233+ Just "Progress" -> do
234+ bs <- readNBytes $ fromIntegral payloadLen
235+ progress <- parseSelectProgress bs
236+ C.yield $ ProgressEventMessage progress
237+ Just "Stats" -> do
238+ bs <- readNBytes $ fromIntegral payloadLen
239+ stats <- parseSelectProgress bs
240+ C.yield $ StatsEventMessage stats
241+ Just "End" -> return ()
242+ _ -> throwIO ESEInvalidMessageType
243+ when (eventHdrValue /= Just "End") handleMessage
244+ Just "error" -> do
245+ let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
246+ maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
247+ _ -> throwIO ESEInvalidMessageType
258248 where
259249 passThrough 0 = return ()
260250 passThrough n = do
261− let c = min n chunkSize
262− b <- readNBytes c
263− C.yield $ RecordPayloadEventMessage b
264− passThrough $ n - B.length b
265−
266−
267−selectProtoConduit :: MonadUnliftIO m
268− => C.ConduitT ByteString EventMessage m ()
251+ let c = min n chunkSize
252+ b <- readNBytes c
253+ C.yield $ RecordPayloadEventMessage b
254+ passThrough $ n - B.length b
255+
256+selectProtoConduit ::
257+ MonadUnliftIO m =>
258+ C.ConduitT ByteString EventMessage m ()
269259selectProtoConduit = crcCheck .| handleMessage
270260
271261-- | selectObjectContent calls the SelectRequest on the given
272262-- object. It returns a Conduit of event messages that can be consumed
273263-- by the client.
274−selectObjectContent :: Bucket -> Object -> SelectRequest
275− -> Minio (C.ConduitT () EventMessage Minio ())
264+selectObjectContent ::
265+ Bucket ->
266+ Object ->
267+ SelectRequest ->
268+ Minio (C.ConduitT () EventMessage Minio ())
276269selectObjectContent b o r = do
277− let reqInfo = defaultS3ReqInfo { riMethod = HT.methodPost
278− , riBucket = Just b
279− , riObject = Just o
280− , riPayload = PayloadBS $ mkSelectRequest r
281− , riNeedsLocation = False
282− , riQueryParams = [("select", Nothing), ("select-type", Just "2")]
283− }
284− --print $ mkSelectRequest r
285− resp <- mkStreamRequest reqInfo
286− return $ NC.responseBody resp .| selectProtoConduit
270+ let reqInfo =
271+ defaultS3ReqInfo
272+ { riMethod = HT.methodPost,
273+ riBucket = Just b,
274+ riObject = Just o,
275+ riPayload = PayloadBS $ mkSelectRequest r,
276+ riNeedsLocation = False,
277+ riQueryParams = [("select", Nothing), ("select-type", Just "2")]
278+ }
279+ --print $ mkSelectRequest r
280+ resp <- mkStreamRequest reqInfo
281+ return $ NC.responseBody resp .| selectProtoConduit
287282
288283-- | A helper conduit that returns only the record payload bytes.
289284getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m ()
290285getPayloadBytes = do
291− evM <- C.await
292− case evM of
293− Just v -> do
294− case v of
295− RecordPayloadEventMessage b -> C.yield b
296− RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
297− _ -> return ()
298− getPayloadBytes
299− Nothing -> return ()
286+ evM <- C.await
287+ case evM of
288+ Just v -> do
289+ case v of
290+ RecordPayloadEventMessage b -> C.yield b
291+ RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
292+ _ -> return ()
293+ getPayloadBytes
294+ Nothing -> return ()
src/Network/Minio/Sign/V4.hs+318−300
@@ -13,57 +13,62 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16+{-# LANGUAGE BangPatterns #-}
1617
1718module Network.Minio.Sign.V4 where
1819
19−import qualified Conduit as C
20−import qualified Data.ByteString as B
21−import qualified Data.ByteString.Base64 as Base64
22−import qualified Data.ByteString.Char8 as B8
23−import Data.CaseInsensitive (mk)
24−import qualified Data.CaseInsensitive as CI
25−import qualified Data.HashMap.Strict as Map
26−import qualified Data.HashSet as Set
27−import qualified Data.Time as Time
28−import qualified Network.HTTP.Conduit as NC
29−import Network.HTTP.Types (Header, parseQuery)
30−import qualified Network.HTTP.Types as H
31−import Text.Printf (printf)
32−
33−import Lib.Prelude
34−
35−import Network.Minio.Data.ByteString
36−import Network.Minio.Data.Crypto
37−import Network.Minio.Data.Time
38−import Network.Minio.Errors
20+import qualified Conduit as C
21+import qualified Data.ByteString as B
22+import qualified Data.ByteString.Base64 as Base64
23+import qualified Data.ByteString.Char8 as B8
24+import qualified Data.ByteString.Lazy as LB
25+import Data.CaseInsensitive (mk)
26+import qualified Data.CaseInsensitive as CI
27+import qualified Data.HashMap.Strict as Map
28+import qualified Data.HashSet as Set
29+import qualified Data.Time as Time
30+import Lib.Prelude
31+import qualified Network.HTTP.Conduit as NC
32+import Network.HTTP.Types (Header, parseQuery)
33+import qualified Network.HTTP.Types as H
34+import Network.Minio.Data.ByteString
35+import Network.Minio.Data.Crypto
36+import Network.Minio.Data.Time
37+import Network.Minio.Errors
38+import Text.Printf (printf)
3939
4040-- these headers are not included in the string to sign when signing a
4141-- request
4242ignoredHeaders :: Set.HashSet ByteString
43−ignoredHeaders = Set.fromList $ map CI.foldedCase
44− [ H.hAuthorization
45− , H.hContentType
46− , H.hUserAgent
47− ]
48−
49−data SignV4Data = SignV4Data {
50− sv4SignTime :: UTCTime
51− , sv4Scope :: ByteString
52− , sv4CanonicalRequest :: ByteString
53− , sv4HeadersToSign :: [(ByteString, ByteString)]
54− , sv4Output :: [(ByteString, ByteString)]
55− , sv4StringToSign :: ByteString
56− , sv4SigningKey :: ByteString
57− } deriving (Show)
58−
59−data SignParams = SignParams {
60− spAccessKey :: Text
61− , spSecretKey :: Text
62− , spTimeStamp :: UTCTime
63− , spRegion :: Maybe Text
64− , spExpirySecs :: Maybe Int
65− , spPayloadHash :: Maybe ByteString
66− } deriving (Show)
43+ignoredHeaders =
44+ Set.fromList $
45+ map
46+ CI.foldedCase
47+ [ H.hAuthorization,
48+ H.hContentType,
49+ H.hUserAgent
50+ ]
51+
52+data SignV4Data = SignV4Data
53+ { sv4SignTime :: UTCTime,
54+ sv4Scope :: ByteString,
55+ sv4CanonicalRequest :: ByteString,
56+ sv4HeadersToSign :: [(ByteString, ByteString)],
57+ sv4Output :: [(ByteString, ByteString)],
58+ sv4StringToSign :: ByteString,
59+ sv4SigningKey :: ByteString
60+ }
61+ deriving (Show)
62+
63+data SignParams = SignParams
64+ { spAccessKey :: Text,
65+ spSecretKey :: Text,
66+ spTimeStamp :: UTCTime,
67+ spRegion :: Maybe Text,
68+ spExpirySecs :: Maybe Int,
69+ spPayloadHash :: Maybe ByteString
70+ }
71+ deriving (Show)
6772
6873debugPrintSignV4Data :: SignV4Data -> IO ()
6974debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
@@ -79,22 +84,23 @@ debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
7984 B8.putStrLn "END of SignV4Data ========="
8085 where
8186 printBytes b = do
82− mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b
87+ mapM_ (\x -> B.putStr $ B.singleton x <> " ") $ B.unpack b
8388 B8.putStrLn ""
8489
8590mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
8691mkAuthHeader accessKey scope signedHeaderKeys sign =
87− let authValue = B.concat
88− [ "AWS4-HMAC-SHA256 Credential="
89− , toS accessKey
90− , "/"
91− , scope
92− , ", SignedHeaders="
93− , signedHeaderKeys
94− , ", Signature="
95− , sign
96− ]
97− in (H.hAuthorization, authValue)
92+ let authValue =
93+ B.concat
94+ [ "AWS4-HMAC-SHA256 Credential=",
95+ toUtf8 accessKey,
96+ "/",
97+ scope,
98+ ", SignedHeaders=",
99+ signedHeaderKeys,
100+ ", Signature=",
101+ sign
102+ ]
103+ in (H.hAuthorization, authValue)
98104
99105-- | Given SignParams and request details, including request method,
100106-- request path, headers, query params and payload hash, generates an
@@ -110,122 +116,132 @@ mkAuthHeader accessKey scope signedHeaderKeys sign =
110116-- the request.
111117signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
112118signV4 !sp !req =
113− let
114− region = fromMaybe "" $ spRegion sp
115− ts = spTimeStamp sp
116− scope = mkScope ts region
117− accessKey = toS $ spAccessKey sp
118− secretKey = toS $ spSecretKey sp
119− expiry = spExpirySecs sp
120− sha256Hdr = ("x-amz-content-sha256",
121− fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp)
122−
123− -- headers to be added to the request
124− datePair = ("X-Amz-Date", awsTimeFormatBS ts)
125− computedHeaders = NC.requestHeaders req ++
126− if isJust $ expiry
127− then []
128− else map (\(x, y) -> (mk x, y)) [datePair, sha256Hdr]
129− headersToSign = getHeadersToSign computedHeaders
130− signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
131−
132− -- query-parameters to be added before signing for presigned URLs
133− -- (i.e. when `isJust expiry`)
134− authQP = [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256")
135− , ("X-Amz-Credential", B.concat [accessKey, "/", scope])
136− , datePair
137− , ("X-Amz-Expires", maybe "" show expiry)
138− , ("X-Amz-SignedHeaders", signedHeaderKeys)
139− ]
140− finalQP = parseQuery (NC.queryString req) ++
141− if isJust expiry
142− then (fmap . fmap) Just authQP
143− else []
144−
145− -- 1. compute canonical request
146− canonicalRequest = mkCanonicalRequest False sp
147− (NC.setQueryString finalQP req)
148− headersToSign
149−
150− -- 2. compute string to sign
151− stringToSign = mkStringToSign ts scope canonicalRequest
152−
153− -- 3.1 compute signing key
154− signingKey = mkSigningKey ts region secretKey
155−
156− -- 3.2 compute signature
157− signature = computeSignature stringToSign signingKey
158−
159− -- 4. compute auth header
160− authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
161−
162− -- finally compute output pairs
163− output = if isJust expiry
164− then ("X-Amz-Signature", signature) : authQP
165− else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,
166− datePair, sha256Hdr]
167−
168− in output
169−
119+ let region = fromMaybe "" $ spRegion sp
120+ ts = spTimeStamp sp
121+ scope = mkScope ts region
122+ accessKey = toUtf8 $ spAccessKey sp
123+ secretKey = toUtf8 $ spSecretKey sp
124+ expiry = spExpirySecs sp
125+ sha256Hdr =
126+ ( "x-amz-content-sha256",
127+ fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
128+ )
129+ -- headers to be added to the request
130+ datePair = ("X-Amz-Date", awsTimeFormatBS ts)
131+ computedHeaders =
132+ NC.requestHeaders req
133+ ++ if isJust $ expiry
134+ then []
135+ else map (\(x, y) -> (mk x, y)) [datePair, sha256Hdr]
136+ headersToSign = getHeadersToSign computedHeaders
137+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
138+ -- query-parameters to be added before signing for presigned URLs
139+ -- (i.e. when `isJust expiry`)
140+ authQP =
141+ [ ("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
142+ ("X-Amz-Credential", B.concat [accessKey, "/", scope]),
143+ datePair,
144+ ("X-Amz-Expires", maybe "" showBS expiry),
145+ ("X-Amz-SignedHeaders", signedHeaderKeys)
146+ ]
147+ finalQP =
148+ parseQuery (NC.queryString req)
149+ ++ if isJust expiry
150+ then (fmap . fmap) Just authQP
151+ else []
152+ -- 1. compute canonical request
153+ canonicalRequest =
154+ mkCanonicalRequest
155+ False
156+ sp
157+ (NC.setQueryString finalQP req)
158+ headersToSign
159+ -- 2. compute string to sign
160+ stringToSign = mkStringToSign ts scope canonicalRequest
161+ -- 3.1 compute signing key
162+ signingKey = mkSigningKey ts region secretKey
163+ -- 3.2 compute signature
164+ signature = computeSignature stringToSign signingKey
165+ -- 4. compute auth header
166+ authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
167+ -- finally compute output pairs
168+ output =
169+ if isJust expiry
170+ then ("X-Amz-Signature", signature) : authQP
171+ else
172+ [ (\(x, y) -> (CI.foldedCase x, y)) authHeader,
173+ datePair,
174+ sha256Hdr
175+ ]
176+ in output
170177
171178mkScope :: UTCTime -> Text -> ByteString
172−mkScope ts region = B.intercalate "/"
173− [ toS $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts
174− , toS region
175− , "s3"
176− , "aws4_request"
177− ]
179+mkScope ts region =
180+ B.intercalate
181+ "/"
182+ [ toUtf8 $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts,
183+ toUtf8 region,
184+ "s3",
185+ "aws4_request"
186+ ]
178187
179188getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
180189getHeadersToSign !h =
181190 filter ((\hdr -> not $ Set.member hdr ignoredHeaders) . fst) $
182− map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
183−
184−mkCanonicalRequest :: Bool -> SignParams -> NC.Request -> [(ByteString, ByteString)]
185− -> ByteString
191+ map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
192+
193+mkCanonicalRequest ::
194+ Bool ->
195+ SignParams ->
196+ NC.Request ->
197+ [(ByteString, ByteString)] ->
198+ ByteString
186199mkCanonicalRequest !isStreaming !sp !req !headersForSign =
187− let
188− canonicalQueryString = B.intercalate "&" $
189− map (\(x, y) -> B.concat [x, "=", y]) $
190− sort $ map (\(x, y) ->
191− (uriEncode True x, maybe "" (uriEncode True) y)) $
192− (parseQuery $ NC.queryString req)
193−
194− sortedHeaders = sort headersForSign
195−
196− canonicalHeaders = B.concat $
197− map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
198−
199− signedHeaders = B.intercalate ";" $ map fst sortedHeaders
200−
201− payloadHashStr =
200+ let canonicalQueryString =
201+ B.intercalate "&"
202+ $ map (\(x, y) -> B.concat [x, "=", y])
203+ $ sort
204+ $ map
205+ ( \(x, y) ->
206+ (uriEncode True x, maybe "" (uriEncode True) y)
207+ )
208+ $ (parseQuery $ NC.queryString req)
209+ sortedHeaders = sort headersForSign
210+ canonicalHeaders =
211+ B.concat $
212+ map (\(x, y) -> B.concat [x, ":", y, "\n"]) sortedHeaders
213+ signedHeaders = B.intercalate ";" $ map fst sortedHeaders
214+ payloadHashStr =
202215 if isStreaming
203− then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
204− else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
205− in
206− B.intercalate "\n"
207− [ NC.method req
208− , uriEncode False $ NC.path req
209− , canonicalQueryString
210− , canonicalHeaders
211− , signedHeaders
212− , payloadHashStr
213− ]
216+ then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
217+ else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
218+ in B.intercalate
219+ "\n"
220+ [ NC.method req,
221+ uriEncode False $ NC.path req,
222+ canonicalQueryString,
223+ canonicalHeaders,
224+ signedHeaders,
225+ payloadHashStr
226+ ]
214227
215228mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
216−mkStringToSign ts !scope !canonicalRequest = B.intercalate "\n"
217− [ "AWS4-HMAC-SHA256"
218− , awsTimeFormatBS ts
219− , scope
220− , hashSHA256 canonicalRequest
221− ]
229+mkStringToSign ts !scope !canonicalRequest =
230+ B.intercalate
231+ "\n"
232+ [ "AWS4-HMAC-SHA256",
233+ awsTimeFormatBS ts,
234+ scope,
235+ hashSHA256 canonicalRequest
236+ ]
222237
223238mkSigningKey :: UTCTime -> Text -> ByteString -> ByteString
224−mkSigningKey ts region !secretKey = hmacSHA256RawBS "aws4_request"
225− . hmacSHA256RawBS "s3"
226− . hmacSHA256RawBS (toS region)
227− . hmacSHA256RawBS (awsDateFormatBS ts)
228− $ B.concat ["AWS4", secretKey]
239+mkSigningKey ts region !secretKey =
240+ hmacSHA256RawBS "aws4_request"
241+ . hmacSHA256RawBS "s3"
242+ . hmacSHA256RawBS (toUtf8 region)
243+ . hmacSHA256RawBS (awsDateFormatBS ts)
244+ $ B.concat ["AWS4", secretKey]
229245
230246computeSignature :: ByteString -> ByteString -> ByteString
231247computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
@@ -233,18 +249,19 @@ computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
233249-- | Takes a validated Post Policy JSON bytestring, the signing time,
234250-- and ConnInfo and returns form-data for the POST upload containing
235251-- just the signature and the encoded post-policy.
236−signV4PostPolicy :: ByteString -> SignParams
237− -> Map.HashMap Text ByteString
252+signV4PostPolicy ::
253+ ByteString ->
254+ SignParams ->
255+ Map.HashMap Text ByteString
238256signV4PostPolicy !postPolicyJSON !sp =
239− let
240− stringToSign = Base64.encode postPolicyJSON
241− region = fromMaybe "" $ spRegion sp
242− signingKey = mkSigningKey (spTimeStamp sp) region $ toS $ spSecretKey sp
243− signature = computeSignature stringToSign signingKey
244− in
245− Map.fromList [ ("x-amz-signature", signature)
246− , ("policy", stringToSign)
247− ]
257+ let stringToSign = Base64.encode postPolicyJSON
258+ region = fromMaybe "" $ spRegion sp
259+ signingKey = mkSigningKey (spTimeStamp sp) region $ toUtf8 $ spSecretKey sp
260+ signature = computeSignature stringToSign signingKey
261+ in Map.fromList
262+ [ ("x-amz-signature", signature),
263+ ("policy", stringToSign)
264+ ]
248265
249266chunkSizeConstant :: Int
250267chunkSizeConstant = 64 * 1024
@@ -252,140 +269,141 @@ chunkSizeConstant = 64 * 1024
252269-- base16Len computes the number of bytes required to represent @n (> 0)@ in
253270-- hexadecimal.
254271base16Len :: Integral a => a -> Int
255−base16Len n | n == 0 = 0
256− | otherwise = 1 + base16Len (n `div` 16)
272+base16Len n
273+ | n == 0 = 0
274+ | otherwise = 1 + base16Len (n `div` 16)
257275
258276signedStreamLength :: Int64 -> Int64
259277signedStreamLength dataLen =
260− let
261− chunkSzInt = fromIntegral chunkSizeConstant
262− (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
263−
264−
265− -- Structure of a chunk:
266− -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
267− encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
268− fullChunkSize = encodedChunkLen chunkSzInt
269− lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
270− finalChunkSize = 1 + 17 + 64 + 2 + 2
271− in
272− numChunks * fullChunkSize + lastChunkSize + finalChunkSize
273−
274−signV4Stream :: Int64 -> SignParams -> NC.Request
275− -> (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
276− -- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
278+ let chunkSzInt = fromIntegral chunkSizeConstant
279+ (numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
280+ -- Structure of a chunk:
281+ -- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
282+ encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
283+ fullChunkSize = encodedChunkLen chunkSzInt
284+ lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
285+ finalChunkSize = 1 + 17 + 64 + 2 + 2
286+ in numChunks * fullChunkSize + lastChunkSize + finalChunkSize
287+
288+signV4Stream ::
289+ Int64 ->
290+ SignParams ->
291+ NC.Request ->
292+ (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
293+-- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
277294signV4Stream !payloadLength !sp !req =
278− let
279− ts = spTimeStamp sp
280−
281− addContentEncoding hs =
295+ let ts = spTimeStamp sp
296+ addContentEncoding hs =
282297 let ceMay = headMay $ filter (\(x, _) -> x == "content-encoding") hs
283− in case ceMay of
284− Nothing -> ("content-encoding", "aws-chunked") : hs
285− Just (_, ce) -> ("content-encoding", ce <> ",aws-chunked") :
286− filter (\(x, _) -> x /= "content-encoding") hs
287−
288− -- headers to be added to the request
289− datePair = ("X-Amz-Date", awsTimeFormatBS ts)
290− computedHeaders = addContentEncoding $
291− datePair : NC.requestHeaders req
292−
293− -- headers specific to streaming signature
294− signedContentLength = signedStreamLength payloadLength
295− streamingHeaders :: [Header]
296− streamingHeaders =
297− [ ("x-amz-decoded-content-length", show payloadLength)
298− , ("content-length", show signedContentLength )
299− , ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
298+ in case ceMay of
299+ Nothing -> ("content-encoding", "aws-chunked") : hs
300+ Just (_, ce) ->
301+ ("content-encoding", ce <> ",aws-chunked")
302+ : filter (\(x, _) -> x /= "content-encoding") hs
303+ -- headers to be added to the request
304+ datePair = ("X-Amz-Date", awsTimeFormatBS ts)
305+ computedHeaders =
306+ addContentEncoding $
307+ datePair : NC.requestHeaders req
308+ -- headers specific to streaming signature
309+ signedContentLength = signedStreamLength payloadLength
310+ streamingHeaders :: [Header]
311+ streamingHeaders =
312+ [ ("x-amz-decoded-content-length", showBS payloadLength),
313+ ("content-length", showBS signedContentLength),
314+ ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
300315 ]
301− headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders
302− signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
303− finalQP = parseQuery (NC.queryString req)
304−
305− -- 1. Compute Seed Signature
306− -- 1.1 Canonical Request
307− canonicalReq = mkCanonicalRequest True sp
308− (NC.setQueryString finalQP req)
309− headersToSign
310−
311− region = fromMaybe "" $ spRegion sp
312− scope = mkScope ts region
313− accessKey = spAccessKey sp
314− secretKey = spSecretKey sp
315−
316− -- 1.2 String toSign
317− stringToSign = mkStringToSign ts scope canonicalReq
318−
319− -- 1.3 Compute signature
320− -- 1.3.1 compute signing key
321− signingKey = mkSigningKey ts region $ toS secretKey
322−
323− -- 1.3.2 Compute signature
324− seedSignature = computeSignature stringToSign signingKey
325−
326− -- 1.3.3 Compute Auth Header
327− authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
328−
329− -- 1.4 Updated headers for the request
330− finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)
331− -- headersToAdd = authHeader : datePair : streamingHeaders
332−
333− toHexStr n = B8.pack $ printf "%x" n
334−
335− (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
336−
337− -- Function to compute string to sign for each chunk.
338− chunkStrToSign prevSign currChunkHash =
339− B.intercalate "\n"
340− [ "AWS4-HMAC-SHA256-PAYLOAD"
341− , awsTimeFormatBS ts
342− , scope
343− , prevSign
344− , hashSHA256 ""
345− , currChunkHash
346− ]
347−
348− -- Read n byte from upstream and return a strict bytestring.
349− mustTakeN n = do
350− bs <- toS <$> (C.takeCE n C..| C.sinkLazy)
316+ headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders
317+ signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
318+ finalQP = parseQuery (NC.queryString req)
319+ -- 1. Compute Seed Signature
320+ -- 1.1 Canonical Request
321+ canonicalReq =
322+ mkCanonicalRequest
323+ True
324+ sp
325+ (NC.setQueryString finalQP req)
326+ headersToSign
327+ region = fromMaybe "" $ spRegion sp
328+ scope = mkScope ts region
329+ accessKey = spAccessKey sp
330+ secretKey = spSecretKey sp
331+ -- 1.2 String toSign
332+ stringToSign = mkStringToSign ts scope canonicalReq
333+ -- 1.3 Compute signature
334+ -- 1.3.1 compute signing key
335+ signingKey = mkSigningKey ts region $ toUtf8 secretKey
336+ -- 1.3.2 Compute signature
337+ seedSignature = computeSignature stringToSign signingKey
338+ -- 1.3.3 Compute Auth Header
339+ authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
340+ -- 1.4 Updated headers for the request
341+ finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)
342+ -- headersToAdd = authHeader : datePair : streamingHeaders
343+
344+ toHexStr n = B8.pack $ printf "%x" n
345+ (numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
346+ -- Function to compute string to sign for each chunk.
347+ chunkStrToSign prevSign currChunkHash =
348+ B.intercalate
349+ "\n"
350+ [ "AWS4-HMAC-SHA256-PAYLOAD",
351+ awsTimeFormatBS ts,
352+ scope,
353+ prevSign,
354+ hashSHA256 "",
355+ currChunkHash
356+ ]
357+ -- Read n byte from upstream and return a strict bytestring.
358+ mustTakeN n = do
359+ bs <- LB.toStrict <$> (C.takeCE n C..| C.sinkLazy)
351360 when (B.length bs /= n) $
352− throwIO MErrVStreamingBodyUnexpectedEOF
361+ throwIO MErrVStreamingBodyUnexpectedEOF
353362 return bs
354−
355− signerConduit n lps prevSign =
356− -- First case encodes a full chunk of length
357− -- 'chunkSizeConstant'.
358− if | n > 0 -> do
359− bs <- mustTakeN chunkSizeConstant
360− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
361− nextSign = computeSignature strToSign signingKey
362− chunkBS = toHexStr chunkSizeConstant
363− <> ";chunk-signature="
364− <> nextSign <> "\r\n" <> bs <> "\r\n"
365− C.yield chunkBS
366− signerConduit (n-1) lps nextSign
367−
368− -- Second case encodes the last chunk which is smaller than
369− -- 'chunkSizeConstant'
370− | lps > 0 -> do
371− bs <- mustTakeN $ fromIntegral lps
372− let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
373− nextSign = computeSignature strToSign signingKey
374− chunkBS = toHexStr lps <> ";chunk-signature="
375− <> nextSign <> "\r\n" <> bs <> "\r\n"
376− C.yield chunkBS
377− signerConduit 0 0 nextSign
378−
379− -- Last case encodes the final signature chunk that has no
380− -- data.
381− | otherwise -> do
382− let strToSign = chunkStrToSign prevSign (hashSHA256 "")
383− nextSign = computeSignature strToSign signingKey
384− lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
385− C.yield lastChunkBS
386− in
387− \src -> req { NC.requestHeaders = finalReqHeaders
388− , NC.requestBody =
389− NC.requestBodySource signedContentLength $
390− src C..| signerConduit numParts lastPSize seedSignature
391− }
363+ signerConduit n lps prevSign =
364+ -- First case encodes a full chunk of length
365+ -- 'chunkSizeConstant'.
366+ if
367+ | n > 0 -> do
368+ bs <- mustTakeN chunkSizeConstant
369+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
370+ nextSign = computeSignature strToSign signingKey
371+ chunkBS =
372+ toHexStr chunkSizeConstant
373+ <> ";chunk-signature="
374+ <> nextSign
375+ <> "\r\n"
376+ <> bs
377+ <> "\r\n"
378+ C.yield chunkBS
379+ signerConduit (n -1) lps nextSign
380+
381+ -- Second case encodes the last chunk which is smaller than
382+ -- 'chunkSizeConstant'
383+ | lps > 0 -> do
384+ bs <- mustTakeN $ fromIntegral lps
385+ let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
386+ nextSign = computeSignature strToSign signingKey
387+ chunkBS =
388+ toHexStr lps <> ";chunk-signature="
389+ <> nextSign
390+ <> "\r\n"
391+ <> bs
392+ <> "\r\n"
393+ C.yield chunkBS
394+ signerConduit 0 0 nextSign
395+
396+ -- Last case encodes the final signature chunk that has no
397+ -- data.
398+ | otherwise -> do
399+ let strToSign = chunkStrToSign prevSign (hashSHA256 "")
400+ nextSign = computeSignature strToSign signingKey
401+ lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
402+ C.yield lastChunkBS
403+ in \src ->
404+ req
405+ { NC.requestHeaders = finalReqHeaders,
406+ NC.requestBody =
407+ NC.requestBodySource signedContentLength $
408+ src C..| signerConduit numParts lastPSize seedSignature
409+ }
src/Network/Minio/Utils.hs+127−98
@@ -16,37 +16,40 @@
1616
1717module Network.Minio.Utils where
1818
19−import qualified Conduit as C
20−import Control.Monad.IO.Unlift (MonadUnliftIO)
21−import qualified Control.Monad.Trans.Resource as R
22−import qualified Data.ByteString as B
23−import qualified Data.ByteString.Lazy as LB
24−import Data.CaseInsensitive (mk, original)
25−import qualified Data.Conduit.Binary as CB
26−import qualified Data.HashMap.Strict as H
27−import qualified Data.List as List
28−import qualified Data.Text as T
29−import Data.Text.Read (decimal)
30−import Data.Time (defaultTimeLocale, parseTimeM,
31− rfc822DateFormat)
32−import Network.HTTP.Conduit (Response)
33−import qualified Network.HTTP.Conduit as NC
34−import qualified Network.HTTP.Types as HT
35−import qualified Network.HTTP.Types.Header as Hdr
36−import qualified System.IO as IO
37−import qualified UnliftIO as U
38−import qualified UnliftIO.Async as A
39−import qualified UnliftIO.MVar as UM
40−
41−import Lib.Prelude
42−
43−import Network.Minio.Data
44−import Network.Minio.Data.ByteString
45−import Network.Minio.JsonParser (parseErrResponseJSON)
46−import Network.Minio.XmlParser (parseErrResponse)
47−
48−allocateReadFile :: (MonadUnliftIO m, R.MonadResource m)
49− => FilePath -> m (R.ReleaseKey, Handle)
19+import qualified Conduit as C
20+import Control.Monad.IO.Unlift (MonadUnliftIO)
21+import qualified Control.Monad.Trans.Resource as R
22+import qualified Data.ByteString as B
23+import qualified Data.ByteString.Lazy as LB
24+import Data.CaseInsensitive (mk, original)
25+import qualified Data.Conduit.Binary as CB
26+import qualified Data.HashMap.Strict as H
27+import qualified Data.List as List
28+import qualified Data.Text as T
29+import Data.Text.Read (decimal)
30+import Data.Time
31+ ( defaultTimeLocale,
32+ parseTimeM,
33+ rfc822DateFormat,
34+ )
35+import Lib.Prelude
36+import Network.HTTP.Conduit (Response)
37+import qualified Network.HTTP.Conduit as NC
38+import qualified Network.HTTP.Types as HT
39+import qualified Network.HTTP.Types.Header as Hdr
40+import Network.Minio.Data
41+import Network.Minio.Data.ByteString
42+import Network.Minio.JsonParser (parseErrResponseJSON)
43+import Network.Minio.XmlParser (parseErrResponse)
44+import qualified System.IO as IO
45+import qualified UnliftIO as U
46+import qualified UnliftIO.Async as A
47+import qualified UnliftIO.MVar as UM
48+
49+allocateReadFile ::
50+ (MonadUnliftIO m, R.MonadResource m) =>
51+ FilePath ->
52+ m (R.ReleaseKey, Handle)
5053allocateReadFile fp = do
5154 (rk, hdlE) <- R.allocate (openReadFile fp) cleanup
5255 either (\(e :: IOException) -> throwIO e) (return . (rk,)) hdlE
@@ -56,30 +59,37 @@ allocateReadFile fp = do
5659
5760-- | Queries the file size from the handle. Catches any file operation
5861-- exceptions and returns Nothing instead.
59−getFileSize :: (MonadUnliftIO m, R.MonadResource m)
60− => Handle -> m (Maybe Int64)
62+getFileSize ::
63+ (MonadUnliftIO m, R.MonadResource m) =>
64+ Handle ->
65+ m (Maybe Int64)
6166getFileSize h = do
6267 resE <- liftIO $ try $ fromIntegral <$> IO.hFileSize h
6368 case resE of
6469 Left (_ :: IOException) -> return Nothing
65− Right s -> return $ Just s
70+ Right s -> return $ Just s
6671
6772-- | Queries if handle is seekable. Catches any file operation
6873-- exceptions and return False instead.
69−isHandleSeekable :: (R.MonadResource m, MonadUnliftIO m)
70− => Handle -> m Bool
74+isHandleSeekable ::
75+ (R.MonadResource m, MonadUnliftIO m) =>
76+ Handle ->
77+ m Bool
7178isHandleSeekable h = do
7279 resE <- liftIO $ try $ IO.hIsSeekable h
7380 case resE of
7481 Left (_ :: IOException) -> return False
75− Right v -> return v
82+ Right v -> return v
7683
7784-- | Helper function that opens a handle to the filepath and performs
7885-- the given action on it. Exceptions of type MError are caught and
7986-- returned - both during file handle allocation and when the action
8087-- is run.
81−withNewHandle :: (MonadUnliftIO m, R.MonadResource m)
82− => FilePath -> (Handle -> m a) -> m (Either IOException a)
88+withNewHandle ::
89+ (MonadUnliftIO m, R.MonadResource m) =>
90+ FilePath ->
91+ (Handle -> m a) ->
92+ m (Either IOException a)
8393withNewHandle fp fileAction = do
8494 -- opening a handle can throw MError exception.
8595 handleE <- try $ allocateReadFile fp
@@ -103,24 +113,27 @@ getETagHeader hs = decodeUtf8Lenient <$> lookupHeader Hdr.hETag hs
103113
104114getMetadata :: [HT.Header] -> [(Text, Text)]
105115getMetadata =
106− map ((\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y)))
116+ map ((\(x, y) -> (decodeUtf8Lenient $ original x, decodeUtf8Lenient $ stripBS y)))
107117
108118toMaybeMetadataHeader :: (Text, Text) -> Maybe (Text, Text)
109119toMaybeMetadataHeader (k, v) =
110− (, v) <$> userMetadataHeaderNameMaybe k
120+ (,v) <$> userMetadataHeaderNameMaybe k
111121
112122getNonUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
113−getNonUserMetadataMap = H.fromList
114− . filter ( isNothing
115− . userMetadataHeaderNameMaybe
116− . fst
117− )
123+getNonUserMetadataMap =
124+ H.fromList
125+ . filter
126+ ( isNothing
127+ . userMetadataHeaderNameMaybe
128+ . fst
129+ )
118130
119131-- | This function collects all headers starting with `x-amz-meta-`
120132-- and strips off this prefix, and returns a map.
121133getUserMetadataMap :: [(Text, Text)] -> H.HashMap Text Text
122−getUserMetadataMap = H.fromList
123− . mapMaybe toMaybeMetadataHeader
134+getUserMetadataMap =
135+ H.fromList
136+ . mapMaybe toMaybeMetadataHeader
124137
125138getLastModifiedHeader :: [HT.Header] -> Maybe UTCTime
126139getLastModifiedHeader hs = do
@@ -132,17 +145,19 @@ getContentLength hs = do
132145 nbs <- decodeUtf8Lenient <$> lookupHeader Hdr.hContentLength hs
133146 fst <$> hush (decimal nbs)
134147
135−
136148decodeUtf8Lenient :: ByteString -> Text
137149decodeUtf8Lenient = decodeUtf8With lenientDecode
138150
139151isSuccessStatus :: HT.Status -> Bool
140−isSuccessStatus sts = let s = HT.statusCode sts
141− in (s >= 200 && s < 300)
142−
143−httpLbs :: MonadIO m
144− => NC.Request -> NC.Manager
145− -> m (NC.Response LByteString)
152+isSuccessStatus sts =
153+ let s = HT.statusCode sts
154+ in (s >= 200 && s < 300)
155+
156+httpLbs ::
157+ MonadIO m =>
158+ NC.Request ->
159+ NC.Manager ->
160+ m (NC.Response LByteString)
146161httpLbs req mgr = do
147162 respE <- liftIO $ tryHttpEx $ NC.httpLbs req mgr
148163 resp <- either throwIO return respE
@@ -154,21 +169,25 @@ httpLbs req mgr = do
154169 Just "application/json" -> do
155170 sErr <- parseErrResponseJSON $ NC.responseBody resp
156171 throwIO sErr
157−
158− _ -> throwIO $ NC.HttpExceptionRequest req $
159− NC.StatusCodeException (void resp) (show resp)
172+ _ ->
173+ throwIO $ NC.HttpExceptionRequest req $
174+ NC.StatusCodeException (void resp) (showBS resp)
160175
161176 return resp
162177 where
163− tryHttpEx :: IO (NC.Response LByteString)
164− -> IO (Either NC.HttpException (NC.Response LByteString))
178+ tryHttpEx ::
179+ IO (NC.Response LByteString) ->
180+ IO (Either NC.HttpException (NC.Response LByteString))
165181 tryHttpEx = try
166− contentTypeMay resp = lookupHeader Hdr.hContentType $
167− NC.responseHeaders resp
168−
169−http :: (MonadUnliftIO m, R.MonadResource m)
170− => NC.Request -> NC.Manager
171− -> m (Response (C.ConduitT () ByteString m ()))
182+ contentTypeMay resp =
183+ lookupHeader Hdr.hContentType $
184+ NC.responseHeaders resp
185+
186+http ::
187+ (MonadUnliftIO m, R.MonadResource m) =>
188+ NC.Request ->
189+ NC.Manager ->
190+ m (Response (C.ConduitT () ByteString m ()))
172191http req mgr = do
173192 respE <- tryHttpEx $ NC.http req mgr
174193 resp <- either throwIO return respE
@@ -178,25 +197,30 @@ http req mgr = do
178197 respBody <- C.connect (NC.responseBody resp) CB.sinkLbs
179198 sErr <- parseErrResponse respBody
180199 throwIO sErr
181−
182200 _ -> do
183201 content <- LB.toStrict . NC.responseBody <$> NC.lbsResponse resp
184202 throwIO $ NC.HttpExceptionRequest req $
185− NC.StatusCodeException (void resp) content
186−
203+ NC.StatusCodeException (void resp) content
187204
188205 return resp
189206 where
190− tryHttpEx :: (MonadUnliftIO m) => m a
191− -> m (Either NC.HttpException a)
207+ tryHttpEx ::
208+ (MonadUnliftIO m) =>
209+ m a ->
210+ m (Either NC.HttpException a)
192211 tryHttpEx = try
193− contentTypeMay resp = lookupHeader Hdr.hContentType $
194− NC.responseHeaders resp
212+ contentTypeMay resp =
213+ lookupHeader Hdr.hContentType $
214+ NC.responseHeaders resp
195215
196216-- Similar to mapConcurrently but limits the number of threads that
197217-- can run using a quantity semaphore.
198−limitedMapConcurrently :: MonadUnliftIO m
199− => Int -> (t -> m a) -> [t] -> m [a]
218+limitedMapConcurrently ::
219+ MonadUnliftIO m =>
220+ Int ->
221+ (t -> m a) ->
222+ [t] ->
223+ m [a]
200224limitedMapConcurrently 0 _ _ = return []
201225limitedMapConcurrently count act args = do
202226 t' <- U.newTVarIO count
@@ -205,17 +229,15 @@ limitedMapConcurrently count act args = do
205229 where
206230 wThread t arg =
207231 U.bracket_ (waitSem t) (signalSem t) $ act arg
208−
209232 -- quantity semaphore implementation using TVar
210233 waitSem t = U.atomically $ do
211234 v <- U.readTVar t
212235 if v > 0
213− then U.writeTVar t (v-1)
214− else U.retrySTM
215−
236+ then U.writeTVar t (v -1)
237+ else U.retrySTM
216238 signalSem t = U.atomically $ do
217239 v <- U.readTVar t
218− U.writeTVar t (v+1)
240+ U.writeTVar t (v + 1)
219241
220242-- helper function to 'drop' empty optional parameter.
221243mkQuery :: Text -> Maybe Text -> Maybe (Text, Text)
@@ -224,7 +246,7 @@ mkQuery k mv = (k,) <$> mv
224246-- helper function to build query parameters that are optional.
225247-- don't use it with mandatory query params with empty value.
226248mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
227−mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
249+mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
228250
229251-- | Conduit that rechunks bytestrings into the given chunk
230252-- lengths. Stops after given chunk lengths are yielded. Stops if
@@ -232,23 +254,30 @@ mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
232254-- received. Does not throw any errors.
233255chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
234256chunkBSConduit [] = return ()
235−chunkBSConduit (s:ss) = do
236− bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
237− if | B.length bs == s -> C.yield bs >> chunkBSConduit ss
238− | B.length bs > 0 -> C.yield bs
239− | otherwise -> return ()
257+chunkBSConduit (s : ss) = do
258+ bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
259+ if
260+ | B.length bs == s -> C.yield bs >> chunkBSConduit ss
261+ | B.length bs > 0 -> C.yield bs
262+ | otherwise -> return ()
240263
241264-- | Select part sizes - the logic is that the minimum part-size will
242265-- be 64MiB.
243266selectPartSizes :: Int64 -> [(PartNumber, Int64, Int64)]
244−selectPartSizes size = uncurry (List.zip3 [1..]) $
245− List.unzip $ loop 0 size
267+selectPartSizes size =
268+ uncurry (List.zip3 [1 ..])
269+ $ List.unzip
270+ $ loop 0 size
246271 where
247272 ceil :: Double -> Int64
248273 ceil = ceiling
249− partSize = max minPartSize (ceil $ fromIntegral size /
250− fromIntegral maxMultipartParts)
251−
274+ partSize =
275+ max
276+ minPartSize
277+ ( ceil $
278+ fromIntegral size
279+ / fromIntegral maxMultipartParts
280+ )
252281 m = fromIntegral partSize
253282 loop st sz
254283 | st > sz = []
@@ -257,16 +286,16 @@ selectPartSizes size = uncurry (List.zip3 [1..]) $
257286
258287lookupRegionCache :: Bucket -> Minio (Maybe Region)
259288lookupRegionCache b = do
260− rMVar <- asks mcRegionMap
261− rMap <- UM.readMVar rMVar
262− return $ H.lookup b rMap
289+ rMVar <- asks mcRegionMap
290+ rMap <- UM.readMVar rMVar
291+ return $ H.lookup b rMap
263292
264293addToRegionCache :: Bucket -> Region -> Minio ()
265294addToRegionCache b region = do
266− rMVar <- asks mcRegionMap
267− UM.modifyMVar_ rMVar $ return . H.insert b region
295+ rMVar <- asks mcRegionMap
296+ UM.modifyMVar_ rMVar $ return . H.insert b region
268297
269298deleteFromRegionCache :: Bucket -> Minio ()
270299deleteFromRegionCache b = do
271− rMVar <- asks mcRegionMap
272− UM.modifyMVar_ rMVar $ return . H.delete b
300+ rMVar <- asks mcRegionMap
301+ UM.modifyMVar_ rMVar $ return . H.delete b
src/Network/Minio/XmlGenerator.hs+158−93
@@ -15,63 +15,80 @@
1515--
1616
1717module Network.Minio.XmlGenerator
18− ( mkCreateBucketConfig
19− , mkCompleteMultipartUploadRequest
20− , mkPutNotificationRequest
21− , mkSelectRequest
22− ) where
23−
18+ ( mkCreateBucketConfig,
19+ mkCompleteMultipartUploadRequest,
20+ mkPutNotificationRequest,
21+ mkSelectRequest,
22+ )
23+where
2424
2525import qualified Data.ByteString.Lazy as LBS
26−import qualified Data.HashMap.Strict as H
27−import qualified Data.Text as T
28−import Text.XML
29−
30−import Lib.Prelude
31−
32−import Network.Minio.Data
33−
26+import qualified Data.HashMap.Strict as H
27+import qualified Data.Text as T
28+import Lib.Prelude
29+import Network.Minio.Data
30+import Text.XML
3431
3532-- | Create a bucketConfig request body XML
3633mkCreateBucketConfig :: Text -> Region -> ByteString
3734mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
3835 where
39− s3Element n = Element (s3Name ns n) mempty
40− root = s3Element "CreateBucketConfiguration"
41− [ NodeElement $ s3Element "LocationConstraint"
42− [ NodeContent location]
36+ s3Element n = Element (s3Name ns n) mempty
37+ root =
38+ s3Element
39+ "CreateBucketConfiguration"
40+ [ NodeElement $
41+ s3Element
42+ "LocationConstraint"
43+ [NodeContent location]
4344 ]
44− bucketConfig = Document (Prologue [] Nothing []) root []
45+ bucketConfig = Document (Prologue [] Nothing []) root []
4546
4647-- | Create a completeMultipartUpload request body XML
4748mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
4849mkCompleteMultipartUploadRequest partInfo =
4950 LBS.toStrict $ renderLBS def cmur
5051 where
51− root = Element "CompleteMultipartUpload" mempty $
52− map (NodeElement . mkPart) partInfo
53− mkPart (n, etag) = Element "Part" mempty
54− [ NodeElement $ Element "PartNumber" mempty
55− [NodeContent $ T.pack $ show n]
56− , NodeElement $ Element "ETag" mempty
57− [NodeContent etag]
58− ]
52+ root =
53+ Element "CompleteMultipartUpload" mempty $
54+ map (NodeElement . mkPart) partInfo
55+ mkPart (n, etag) =
56+ Element
57+ "Part"
58+ mempty
59+ [ NodeElement $
60+ Element
61+ "PartNumber"
62+ mempty
63+ [NodeContent $ T.pack $ show n],
64+ NodeElement $
65+ Element
66+ "ETag"
67+ mempty
68+ [NodeContent etag]
69+ ]
5970 cmur = Document (Prologue [] Nothing []) root []
6071
6172-- Simplified XML representation without element attributes.
62−data XNode = XNode Text [XNode]
63− | XLeaf Text Text
73+data XNode
74+ = XNode Text [XNode]
75+ | XLeaf Text Text
6476 deriving (Eq, Show)
6577
6678toXML :: Text -> XNode -> ByteString
67−toXML ns node = LBS.toStrict $ renderLBS def $
68− Document (Prologue [] Nothing []) (xmlNode node) []
79+toXML ns node =
80+ LBS.toStrict $ renderLBS def $
81+ Document (Prologue [] Nothing []) (xmlNode node) []
6982 where
7083 xmlNode :: XNode -> Element
71− xmlNode (XNode name nodes) = Element (s3Name ns name) mempty $
72− map (NodeElement . xmlNode) nodes
73− xmlNode (XLeaf name content) = Element (s3Name ns name) mempty
74− [NodeContent content]
84+ xmlNode (XNode name nodes) =
85+ Element (s3Name ns name) mempty $
86+ map (NodeElement . xmlNode) nodes
87+ xmlNode (XLeaf name content) =
88+ Element
89+ (s3Name ns name)
90+ mempty
91+ [NodeContent content]
7592
7693class ToXNode a where
7794 toXNode :: a -> XNode
@@ -80,24 +97,29 @@ instance ToXNode Event where
8097 toXNode = XLeaf "Event" . show
8198
8299instance ToXNode Notification where
83− toXNode (Notification qc tc lc) = XNode "NotificationConfiguration" $
84− map (toXNodesWithArnName "QueueConfiguration" "Queue") qc ++
85− map (toXNodesWithArnName "TopicConfiguration" "Topic") tc ++
86− map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
100+ toXNode (Notification qc tc lc) =
101+ XNode "NotificationConfiguration" $
102+ map (toXNodesWithArnName "QueueConfiguration" "Queue") qc
103+ ++ map (toXNodesWithArnName "TopicConfiguration" "Topic") tc
104+ ++ map (toXNodesWithArnName "CloudFunctionConfiguration" "CloudFunction") lc
87105
88106toXNodesWithArnName :: Text -> Text -> NotificationConfig -> XNode
89107toXNodesWithArnName eltName arnName (NotificationConfig id arn events fRule) =
90− XNode eltName $ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events ++
91− [toXNode fRule]
108+ XNode eltName $
109+ [XLeaf "Id" id, XLeaf arnName arn] ++ map toXNode events
110+ ++ [toXNode fRule]
92111
93112instance ToXNode Filter where
94113 toXNode (Filter (FilterKey (FilterRules rules))) =
95114 XNode "Filter" [XNode "S3Key" (map getFRXNode rules)]
96115
97116getFRXNode :: FilterRule -> XNode
98−getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n
99− , XLeaf "Value" v
100− ]
117+getFRXNode (FilterRule n v) =
118+ XNode
119+ "FilterRule"
120+ [ XLeaf "Name" n,
121+ XLeaf "Value" v
122+ ]
101123
102124mkPutNotificationRequest :: Text -> Notification -> ByteString
103125mkPutNotificationRequest ns = toXML ns . toXNode
@@ -106,60 +128,103 @@ mkSelectRequest :: SelectRequest -> ByteString
106128mkSelectRequest r = LBS.toStrict $ renderLBS def sr
107129 where
108130 sr = Document (Prologue [] Nothing []) root []
109− root = Element "SelectRequest" mempty $
110− [ NodeElement (Element "Expression" mempty
111− [NodeContent $ srExpression r])
112− , NodeElement (Element "ExpressionType" mempty
113− [NodeContent $ show $ srExpressionType r])
114− , NodeElement (Element "InputSerialization" mempty $
115− inputSerializationNodes $ srInputSerialization r)
116− , NodeElement (Element "OutputSerialization" mempty $
117− outputSerializationNodes $ srOutputSerialization r)
118− ] ++ maybe [] reqProgElem (srRequestProgressEnabled r)
119− reqProgElem enabled = [NodeElement
120− (Element "RequestProgress" mempty
121− [NodeElement
122− (Element "Enabled" mempty
123− [NodeContent
124− (if enabled then "TRUE" else "FALSE")]
125− )
126− ]
127− )
128− ]
129− inputSerializationNodes is = comprTypeNode (isCompressionType is) ++
130− [NodeElement $ formatNode (isFormatInfo is)]
131− comprTypeNode (Just c) = [NodeElement $ Element "CompressionType" mempty
132− [NodeContent $ case c of
133− CompressionTypeNone -> "NONE"
134− CompressionTypeGzip -> "GZIP"
135− CompressionTypeBzip2 -> "BZIP2"
136− ]
137− ]
131+ root =
132+ Element "SelectRequest" mempty $
133+ [ NodeElement
134+ ( Element
135+ "Expression"
136+ mempty
137+ [NodeContent $ srExpression r]
138+ ),
139+ NodeElement
140+ ( Element
141+ "ExpressionType"
142+ mempty
143+ [NodeContent $ show $ srExpressionType r]
144+ ),
145+ NodeElement
146+ ( Element "InputSerialization" mempty
147+ $ inputSerializationNodes
148+ $ srInputSerialization r
149+ ),
150+ NodeElement
151+ ( Element "OutputSerialization" mempty
152+ $ outputSerializationNodes
153+ $ srOutputSerialization r
154+ )
155+ ]
156+ ++ maybe [] reqProgElem (srRequestProgressEnabled r)
157+ reqProgElem enabled =
158+ [ NodeElement
159+ ( Element
160+ "RequestProgress"
161+ mempty
162+ [ NodeElement
163+ ( Element
164+ "Enabled"
165+ mempty
166+ [ NodeContent
167+ (if enabled then "TRUE" else "FALSE")
168+ ]
169+ )
170+ ]
171+ )
172+ ]
173+ inputSerializationNodes is =
174+ comprTypeNode (isCompressionType is)
175+ ++ [NodeElement $ formatNode (isFormatInfo is)]
176+ comprTypeNode (Just c) =
177+ [ NodeElement $
178+ Element
179+ "CompressionType"
180+ mempty
181+ [ NodeContent $ case c of
182+ CompressionTypeNone -> "NONE"
183+ CompressionTypeGzip -> "GZIP"
184+ CompressionTypeBzip2 -> "BZIP2"
185+ ]
186+ ]
138187 comprTypeNode Nothing = []
139−
140188 kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
141189 formatNode (InputFormatCSV (CSVProp h)) =
142− Element "CSV" mempty
143− (map NodeElement $ map kvElement $ H.toList h)
190+ Element
191+ "CSV"
192+ mempty
193+ (map NodeElement $ map kvElement $ H.toList h)
144194 formatNode (InputFormatJSON p) =
145− Element "JSON" mempty
146− [NodeElement
147− (Element "Type" mempty
148− [NodeContent $ case jsonipType p of
149− JSONTypeDocument -> "DOCUMENT"
150− JSONTypeLines -> "LINES"
151− ]
152− )
153− ]
195+ Element
196+ "JSON"
197+ mempty
198+ [ NodeElement
199+ ( Element
200+ "Type"
201+ mempty
202+ [ NodeContent $ case jsonipType p of
203+ JSONTypeDocument -> "DOCUMENT"
204+ JSONTypeLines -> "LINES"
205+ ]
206+ )
207+ ]
154208 formatNode InputFormatParquet = Element "Parquet" mempty []
155−
156209 outputSerializationNodes (OutputSerializationJSON j) =
157− [NodeElement (Element "JSON" mempty $
158− rdElem $ jsonopRecordDelimiter j)]
210+ [ NodeElement
211+ ( Element "JSON" mempty
212+ $ rdElem
213+ $ jsonopRecordDelimiter j
214+ )
215+ ]
159216 outputSerializationNodes (OutputSerializationCSV (CSVProp h)) =
160− [NodeElement $ Element "CSV" mempty
161− (map NodeElement $ map kvElement $ H.toList h)]
162−
217+ [ NodeElement $
218+ Element
219+ "CSV"
220+ mempty
221+ (map NodeElement $ map kvElement $ H.toList h)
222+ ]
163223 rdElem Nothing = []
164− rdElem (Just t) = [NodeElement $ Element "RecordDelimiter" mempty
165− [NodeContent t]]
224+ rdElem (Just t) =
225+ [ NodeElement $
226+ Element
227+ "RecordDelimiter"
228+ mempty
229+ [NodeContent t]
230+ ]
src/Network/Minio/XmlParser.hs+106−114
@@ -15,34 +15,32 @@
1515--
1616
1717module Network.Minio.XmlParser
18− ( parseListBuckets
19− , parseLocation
20− , parseNewMultipartUpload
21− , parseCompleteMultipartUploadResponse
22− , parseCopyObjectResponse
23− , parseListObjectsResponse
24− , parseListObjectsV1Response
25− , parseListUploadsResponse
26− , parseListPartsResponse
27− , parseErrResponse
28− , parseNotification
29− , parseSelectProgress
30− ) where
18+ ( parseListBuckets,
19+ parseLocation,
20+ parseNewMultipartUpload,
21+ parseCompleteMultipartUploadResponse,
22+ parseCopyObjectResponse,
23+ parseListObjectsResponse,
24+ parseListObjectsV1Response,
25+ parseListUploadsResponse,
26+ parseListPartsResponse,
27+ parseErrResponse,
28+ parseNotification,
29+ parseSelectProgress,
30+ )
31+where
3132
3233import qualified Data.ByteString.Lazy as LB
33−import qualified Data.HashMap.Strict as H
34−import Data.List (zip3, zip4, zip6)
35−import qualified Data.Text as T
36−import Data.Text.Read (decimal)
37−import Data.Time
38−import Text.XML
39−import Text.XML.Cursor hiding (bool)
40−
41−import Lib.Prelude
42−
43−import Network.Minio.Data
44−import Network.Minio.Errors
45−
34+import qualified Data.HashMap.Strict as H
35+import Data.List (zip3, zip4, zip6)
36+import qualified Data.Text as T
37+import Data.Text.Read (decimal)
38+import Data.Time
39+import Lib.Prelude
40+import Network.Minio.Data
41+import Network.Minio.Errors
42+import Text.XML
43+import Text.XML.Cursor hiding (bool)
4644
4745-- | Represent the time format string returned by S3 API calls.
4846s3TimeFormat :: [Char]
@@ -58,12 +56,14 @@ uncurry6 f (a, b, c, d, e, g) = f a b c d e g
5856-- | Parse time strings from XML
5957parseS3XMLTime :: MonadIO m => Text -> m UTCTime
6058parseS3XMLTime t =
61− maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return $
62− parseTimeM True defaultTimeLocale s3TimeFormat $ T.unpack t
59+ maybe (throwIO $ MErrVXmlParse $ "timestamp parse failure: " <> t) return
60+ $ parseTimeM True defaultTimeLocale s3TimeFormat
61+ $ T.unpack t
6362
6463parseDecimal :: (MonadIO m, Integral a) => Text -> m a
65−parseDecimal numStr = either (throwIO . MErrVXmlParse . show) return $
66− fst <$> decimal numStr
64+parseDecimal numStr =
65+ either (throwIO . MErrVXmlParse . show) return $
66+ fst <$> decimal numStr
6767
6868parseDecimals :: (MonadIO m, Integral a) => [Text] -> m [a]
6969parseDecimals numStr = forM numStr parseDecimal
@@ -72,18 +72,18 @@ s3Elem :: Text -> Text -> Axis
7272s3Elem ns = element . s3Name ns
7373
7474parseRoot :: (MonadIO m) => LByteString -> m Cursor
75−parseRoot = either (throwIO . MErrVXmlParse . show) (return . fromDocument)
76− . parseLBS def
75+parseRoot =
76+ either (throwIO . MErrVXmlParse . show) (return . fromDocument)
77+ . parseLBS def
7778
7879-- | Parse the response XML of a list buckets call.
7980parseListBuckets :: (MonadReader env m, HasSvcNamespace env, MonadIO m) => LByteString -> m [BucketInfo]
8081parseListBuckets xmldata = do
8182 r <- parseRoot xmldata
8283 ns <- asks getSvcNamespace
83− let
84− s3Elem' = s3Elem ns
85− names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
86− timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
84+ let s3Elem' = s3Elem ns
85+ names = r $// s3Elem' "Bucket" &// s3Elem' "Name" &/ content
86+ timeStrings = r $// s3Elem' "Bucket" &// s3Elem' "CreationDate" &/ content
8787
8888 times <- mapM parseS3XMLTime timeStrings
8989 return $ zipWith BucketInfo names times
@@ -116,41 +116,38 @@ parseCopyObjectResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =
116116parseCopyObjectResponse xmldata = do
117117 r <- parseRoot xmldata
118118 ns <- asks getSvcNamespace
119− let
120− s3Elem' = s3Elem ns
121− mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
119+ let s3Elem' = s3Elem ns
120+ mtimeStr = T.concat $ r $// s3Elem' "LastModified" &/ content
122121
123122 mtime <- parseS3XMLTime mtimeStr
124123 return (T.concat $ r $// s3Elem' "ETag" &/ content, mtime)
125124
126125-- | Parse the response XML of a list objects v1 call.
127−parseListObjectsV1Response :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
128− => LByteString -> m ListObjectsV1Result
126+parseListObjectsV1Response ::
127+ (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
128+ LByteString ->
129+ m ListObjectsV1Result
129130parseListObjectsV1Response xmldata = do
130131 r <- parseRoot xmldata
131132 ns <- asks getSvcNamespace
132− let
133− s3Elem' = s3Elem ns
134− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
135−
136− nextMarker = headMay $ r $/ s3Elem' "NextMarker" &/ content
137−
138− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
139−
140− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
141− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
142− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
143− -- if response xml contains empty etag response fill them with as
144− -- many empty Text for the zip4 below to work as intended.
145− etags = etagsList ++ repeat ""
146− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
133+ let s3Elem' = s3Elem ns
134+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
135+ nextMarker = headMay $ r $/ s3Elem' "NextMarker" &/ content
136+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
137+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
138+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
139+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
140+ -- if response xml contains empty etag response fill them with as
141+ -- many empty Text for the zip4 below to work as intended.
142+ etags = etagsList ++ repeat ""
143+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
147144
148145 modTimes <- mapM parseS3XMLTime modTimeStr
149146 sizes <- parseDecimals sizeStr
150147
151− let
152− objects = map (uncurry6 ObjectInfo) $
153− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
148+ let objects =
149+ map (uncurry6 ObjectInfo) $
150+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
154151
155152 return $ ListObjectsV1Result hasMore nextMarker objects prefixes
156153
@@ -159,28 +156,24 @@ parseListObjectsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
159156parseListObjectsResponse xmldata = do
160157 r <- parseRoot xmldata
161158 ns <- asks getSvcNamespace
162− let
163− s3Elem' = s3Elem ns
164− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
165−
166− nextToken = headMay $ r $/ s3Elem' "NextContinuationToken" &/ content
167−
168− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
169−
170− keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
171− modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
172− etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
173− -- if response xml contains empty etag response fill them with as
174− -- many empty Text for the zip4 below to work as intended.
175− etags = etagsList ++ repeat ""
176− sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
159+ let s3Elem' = s3Elem ns
160+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
161+ nextToken = headMay $ r $/ s3Elem' "NextContinuationToken" &/ content
162+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
163+ keys = r $/ s3Elem' "Contents" &/ s3Elem' "Key" &/ content
164+ modTimeStr = r $/ s3Elem' "Contents" &/ s3Elem' "LastModified" &/ content
165+ etagsList = r $/ s3Elem' "Contents" &/ s3Elem' "ETag" &/ content
166+ -- if response xml contains empty etag response fill them with as
167+ -- many empty Text for the zip4 below to work as intended.
168+ etags = etagsList ++ repeat ""
169+ sizeStr = r $/ s3Elem' "Contents" &/ s3Elem' "Size" &/ content
177170
178171 modTimes <- mapM parseS3XMLTime modTimeStr
179172 sizes <- parseDecimals sizeStr
180173
181− let
182− objects = map (uncurry6 ObjectInfo) $
183− zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
174+ let objects =
175+ map (uncurry6 ObjectInfo) $
176+ zip6 keys modTimes etags sizes (repeat H.empty) (repeat H.empty)
184177
185178 return $ ListObjectsResult hasMore nextToken objects prefixes
186179
@@ -189,20 +182,18 @@ parseListUploadsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m)
189182parseListUploadsResponse xmldata = do
190183 r <- parseRoot xmldata
191184 ns <- asks getSvcNamespace
192− let
193− s3Elem' = s3Elem ns
194− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
195− prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
196− nextKey = headMay $ r $/ s3Elem' "NextKeyMarker" &/ content
197− nextUpload = headMay $ r $/ s3Elem' "NextUploadIdMarker" &/ content
198− uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
199− uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
200− uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
185+ let s3Elem' = s3Elem ns
186+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
187+ prefixes = r $/ s3Elem' "CommonPrefixes" &/ s3Elem' "Prefix" &/ content
188+ nextKey = headMay $ r $/ s3Elem' "NextKeyMarker" &/ content
189+ nextUpload = headMay $ r $/ s3Elem' "NextUploadIdMarker" &/ content
190+ uploadKeys = r $/ s3Elem' "Upload" &/ s3Elem' "Key" &/ content
191+ uploadIds = r $/ s3Elem' "Upload" &/ s3Elem' "UploadId" &/ content
192+ uploadInitTimeStr = r $/ s3Elem' "Upload" &/ s3Elem' "Initiated" &/ content
201193
202194 uploadInitTimes <- mapM parseS3XMLTime uploadInitTimeStr
203195
204− let
205− uploads = zip3 uploadKeys uploadIds uploadInitTimes
196+ let uploads = zip3 uploadKeys uploadIds uploadInitTimes
206197
207198 return $ ListUploadsResult hasMore nextKey nextUpload uploads prefixes
208199
@@ -210,27 +201,25 @@ parseListPartsResponse :: (MonadReader env m, HasSvcNamespace env, MonadIO m) =>
210201parseListPartsResponse xmldata = do
211202 r <- parseRoot xmldata
212203 ns <- asks getSvcNamespace
213− let
214− s3Elem' = s3Elem ns
215− hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
216− nextPartNumStr = headMay $ r $/ s3Elem' "NextPartNumberMarker" &/ content
217− partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
218− partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
219− partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
220− partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
204+ let s3Elem' = s3Elem ns
205+ hasMore = ["true"] == (r $/ s3Elem' "IsTruncated" &/ content)
206+ nextPartNumStr = headMay $ r $/ s3Elem' "NextPartNumberMarker" &/ content
207+ partNumberStr = r $/ s3Elem' "Part" &/ s3Elem' "PartNumber" &/ content
208+ partModTimeStr = r $/ s3Elem' "Part" &/ s3Elem' "LastModified" &/ content
209+ partETags = r $/ s3Elem' "Part" &/ s3Elem' "ETag" &/ content
210+ partSizeStr = r $/ s3Elem' "Part" &/ s3Elem' "Size" &/ content
221211
222212 partModTimes <- mapM parseS3XMLTime partModTimeStr
223213 partSizes <- parseDecimals partSizeStr
224214 partNumbers <- parseDecimals partNumberStr
225215 nextPartNum <- parseDecimals $ maybeToList nextPartNumStr
226216
227− let
228− partInfos = map (uncurry4 ObjectPartInfo) $
229− zip4 partNumbers partETags partSizes partModTimes
217+ let partInfos =
218+ map (uncurry4 ObjectPartInfo) $
219+ zip4 partNumbers partETags partSizes partModTimes
230220
231221 return $ ListPartsResult hasMore (listToMaybe nextPartNum) partInfos
232222
233−
234223parseErrResponse :: (MonadIO m) => LByteString -> m ServiceErr
235224parseErrResponse xmldata = do
236225 r <- parseRoot xmldata
@@ -250,28 +239,31 @@ parseNotification xmldata = do
250239 <*> (mapM (parseNode ns "Topic") tcfg)
251240 <*> (mapM (parseNode ns "CloudFunction") lcfg)
252241 where
253−
254242 getFilterRule ns c =
255243 let name = T.concat $ c $/ s3Elem ns "Name" &/ content
256244 value = T.concat $ c $/ s3Elem ns "Value" &/ content
257− in FilterRule name value
258−
245+ in FilterRule name value
259246 parseNode ns arnName nodeData = do
260247 let c = fromNode nodeData
261248 id = T.concat $ c $/ s3Elem ns "Id" &/ content
262249 arn = T.concat $ c $/ s3Elem ns arnName &/ content
263250 events = catMaybes $ map textToEvent $ c $/ s3Elem ns "Event" &/ content
264− rules = c $/ s3Elem ns "Filter" &/ s3Elem ns "S3Key" &/
265− s3Elem ns "FilterRule" &| getFilterRule ns
266− return $ NotificationConfig id arn events
267− (Filter $ FilterKey $ FilterRules rules)
251+ rules =
252+ c $/ s3Elem ns "Filter" &/ s3Elem ns "S3Key"
253+ &/ s3Elem ns "FilterRule" &| getFilterRule ns
254+ return $
255+ NotificationConfig
256+ id
257+ arn
258+ events
259+ (Filter $ FilterKey $ FilterRules rules)
268260
269261parseSelectProgress :: MonadIO m => ByteString -> m Progress
270262parseSelectProgress xmldata = do
271− r <- parseRoot $ LB.fromStrict xmldata
272− let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
273− bProcessed = T.concat $ r $/element "BytesProcessed" &/ content
274− bReturned = T.concat $ r $/element "BytesReturned" &/ content
275− Progress <$> parseDecimal bScanned
276− <*> parseDecimal bProcessed
277− <*> parseDecimal bReturned
263+ r <- parseRoot $ LB.fromStrict xmldata
264+ let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
265+ bProcessed = T.concat $ r $/ element "BytesProcessed" &/ content
266+ bReturned = T.concat $ r $/ element "BytesReturned" &/ content
267+ Progress <$> parseDecimal bScanned
268+ <*> parseDecimal bProcessed
269+ <*> parseDecimal bReturned
stack.yaml+4−2
@@ -15,7 +15,7 @@
1515# resolver:
1616# name: custom-snapshot
1717# location: "./custom-snapshot.yaml"
18−resolver: lts-14.6
18+resolver: lts-16.0
1919
2020# User packages to be built.
2121# Various formats can be used as shown in the example below.
@@ -39,7 +39,9 @@ packages:
3939- '.'
4040# Dependency packages to be pulled from upstream that are not in the resolver
4141# (e.g., acme-missiles-0.3)
42−extra-deps: []
42+extra-deps:
43+- unliftio-core-0.2.0.1
44+- protolude-0.3.0
4345
4446# Override default flag values for local packages and extra-deps
4547flags: {}
stack.yaml.lock+19−5
@@ -3,10 +3,24 @@
33# For more information, please see the documentation at:
44# https://docs.haskellstack.org/en/stable/lock_files
55
6−packages: []
6+packages:
7+- completed:
8+ hackage: unliftio-core-0.2.0.1@sha256:9b3e44ea9aacacbfc35b3b54015af450091916ac3618a41868ebf6546977659a,1082
9+ pantry-tree:
10+ size: 328
11+ sha256: e81c5a1e82ec2cd68cbbbec9cd60567363abe02257fa1370a906f6754b6818b8
12+ original:
13+ hackage: unliftio-core-0.2.0.1
14+- completed:
15+ hackage: protolude-0.3.0@sha256:8361b811b420585b122a7ba715aa5923834db6e8c36309bf267df2dbf66b95ef,2693
16+ pantry-tree:
17+ size: 1644
18+ sha256: babf32b414f25f790b7a4ce6bae5c960bc51a11a289e7c47335b222e6762560c
19+ original:
20+ hackage: protolude-0.3.0
721snapshots:
822- completed:
9− size: 524127
10− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/14/6.yaml
11− sha256: dc70dfb45e2c32f54719819bd055f46855dd4b3bd2e58b9f3f38729a2d553fbb
12− original: lts-14.6
23+ size: 531237
24+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/0.yaml
25+ sha256: 210e15b7043e2783115afe16b0d54914b1611cdaa73f3ca3ca7f8e0847ff54e5
26+ original: lts-16.0
test/LiveServer.hs+623−456
@@ -1,4 +1,5 @@
11{-# LANGUAGE OverloadedStrings #-}
2+
23--
34-- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
45--
@@ -15,37 +16,34 @@
1516-- limitations under the License.
1617--
1718
18−import qualified Test.QuickCheck as Q
19−import Test.Tasty
20−import Test.Tasty.HUnit
21−import Test.Tasty.QuickCheck as QC
22−
23−import Conduit (replicateC)
24−import qualified Conduit as C
25−import qualified Control.Monad.Trans.Resource as R
26−import qualified Data.ByteString as BS
27−import Data.Conduit (yield)
28−import qualified Data.Conduit.Binary as CB
29−import Data.Conduit.Combinators (sinkList)
30−import qualified Data.HashMap.Strict as H
31−import qualified Data.Text as T
32−import Data.Time (fromGregorian)
33−import qualified Data.Time as Time
19+import Conduit (replicateC)
20+import qualified Conduit as C
21+import qualified Control.Monad.Trans.Resource as R
22+import qualified Data.ByteString as BS
23+import Data.Conduit (yield)
24+import qualified Data.Conduit.Binary as CB
25+import Data.Conduit.Combinators (sinkList)
26+import qualified Data.HashMap.Strict as H
27+import qualified Data.Text as T
28+import Data.Time (fromGregorian)
29+import qualified Data.Time as Time
30+import Lib.Prelude
3431import qualified Network.HTTP.Client.MultipartFormData as Form
35−import qualified Network.HTTP.Conduit as NC
36−import qualified Network.HTTP.Types as HT
37−import System.Directory (getTemporaryDirectory)
38−import System.Environment (lookupEnv)
39−import qualified System.IO as SIO
40−
41−import Lib.Prelude
42−
43−import Network.Minio
44−import Network.Minio.Data
45−import Network.Minio.Data.Crypto
46−import Network.Minio.PutObject
47−import Network.Minio.S3API
48−import Network.Minio.Utils
32+import qualified Network.HTTP.Conduit as NC
33+import qualified Network.HTTP.Types as HT
34+import Network.Minio
35+import Network.Minio.Data
36+import Network.Minio.Data.Crypto
37+import Network.Minio.PutObject
38+import Network.Minio.S3API
39+import Network.Minio.Utils
40+import System.Directory (getTemporaryDirectory)
41+import System.Environment (lookupEnv)
42+import qualified System.IO as SIO
43+import qualified Test.QuickCheck as Q
44+import Test.Tasty
45+import Test.Tasty.HUnit
46+import Test.Tasty.QuickCheck as QC
4947
5048main :: IO ()
5149main = defaultMain tests
@@ -57,17 +55,20 @@ tests = testGroup "Tests" [liveServerUnitTests]
5755randomDataSrc :: MonadIO m => Int64 -> C.ConduitM () ByteString m ()
5856randomDataSrc s' = genBS s'
5957 where
60− concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++
61− [BS.take (fromIntegral r) bs]
62− where (q, r) = n `divMod` fromIntegral (BS.length bs)
63−
58+ concatIt bs n =
59+ BS.concat $
60+ replicate (fromIntegral q) bs
61+ ++ [BS.take (fromIntegral r) bs]
62+ where
63+ (q, r) = n `divMod` fromIntegral (BS.length bs)
6464 genBS s = do
6565 w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255))
6666 let byteArr64 = BS.pack w8s
6767 if s < oneMiB
6868 then yield $ concatIt byteArr64 s
69− else do yield $ concatIt byteArr64 oneMiB
70− genBS (s - oneMiB)
69+ else do
70+ yield $ concatIt byteArr64 oneMiB
71+ genBS (s - oneMiB)
7172
7273mkRandFile :: R.MonadResource m => Int64 -> m FilePath
7374mkRandFile size = do
@@ -79,15 +80,17 @@ funTestBucketPrefix = "miniohstest-"
7980
8081loadTestServer :: IO ConnectInfo
8182loadTestServer = do
82− val <- lookupEnv "MINIO_LOCAL"
83− isSecure <- lookupEnv "MINIO_SECURE"
84− return $ case (val, isSecure) of
85− (Just _, Just _) -> setCreds (Credentials "minio" "minio123") "https://localhost:9000"
86− (Just _, Nothing) -> setCreds (Credentials "minio" "minio123") "http://localhost:9000"
87− (Nothing, _) -> minioPlayCI
88−
89−funTestWithBucket :: TestName
90− -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree
83+ val <- lookupEnv "MINIO_LOCAL"
84+ isSecure <- lookupEnv "MINIO_SECURE"
85+ return $ case (val, isSecure) of
86+ (Just _, Just _) -> setCreds (Credentials "minio" "minio123") "https://localhost:9000"
87+ (Just _, Nothing) -> setCreds (Credentials "minio" "minio123") "http://localhost:9000"
88+ (Nothing, _) -> minioPlayCI
89+
90+funTestWithBucket ::
91+ TestName ->
92+ (([Char] -> Minio ()) -> Bucket -> Minio ()) ->
93+ TestTree
9194funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
9295 -- generate a random name for the bucket
9396 bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z'))
@@ -104,47 +107,51 @@ funTestWithBucket t minioTest = testCaseSteps t $ \step -> do
104107 isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret)
105108
106109liveServerUnitTests :: TestTree
107−liveServerUnitTests = testGroup "Unit tests against a live server"
108− [ basicTests
109− , listingTest
110− , highLevelListingTest
111− , lowLevelMultipartTest
112− , putObjectSizeTest
113− , putObjectNoSizeTest
114− , multipartTest
115− , putObjectContentTypeTest
116− , putObjectContentLanguageTest
117− , putObjectStorageClassTest
118− , putObjectUserMetadataTest
119− , getObjectTest
120− , copyObjectTests
121− , presignedUrlFunTest
122− , presignedPostPolicyFunTest
123− , bucketPolicyFunTest
124− , getNPutSSECTest
125− ]
110+liveServerUnitTests =
111+ testGroup
112+ "Unit tests against a live server"
113+ [ basicTests,
114+ listingTest,
115+ highLevelListingTest,
116+ lowLevelMultipartTest,
117+ putObjectSizeTest,
118+ putObjectNoSizeTest,
119+ multipartTest,
120+ putObjectContentTypeTest,
121+ putObjectContentLanguageTest,
122+ putObjectStorageClassTest,
123+ putObjectUserMetadataTest,
124+ getObjectTest,
125+ copyObjectTests,
126+ presignedUrlFunTest,
127+ presignedPostPolicyFunTest,
128+ bucketPolicyFunTest,
129+ getNPutSSECTest
130+ ]
126131
127132basicTests :: TestTree
128133basicTests = funTestWithBucket "Basic tests" $
129134 \step bucket -> do
130135 step "getService works and contains the test bucket."
131136 buckets <- getService
132− unless (length (filter (== bucket) $ map biName buckets) == 1) $
133− liftIO $
134− assertFailure ("The bucket " ++ show bucket ++
135− " was expected to exist.")
137+ unless (length (filter (== bucket) $ map biName buckets) == 1)
138+ $ liftIO
139+ $ assertFailure
140+ ( "The bucket " ++ show bucket
141+ ++ " was expected to exist."
142+ )
136143
137144 step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised."
138145 mbE <- try $ makeBucket bucket Nothing
139146 case mbE of
140147 Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou
141− _ -> return ()
148+ _ -> return ()
142149
143150 step "makeBucket with an invalid bucket name and check for appropriate exception."
144151 invalidMBE <- try $ makeBucket "invalidBucketName" Nothing
145152 case invalidMBE of
146153 Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName"
147− _ -> return ()
154+ _ -> return ()
148155
149156 step "getLocation works"
150157 region <- getLocation bucket
@@ -157,7 +164,7 @@ basicTests = funTestWithBucket "Basic tests" $
157164 fpE <- try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release" defaultPutObjectOptions
158165 case fpE of
159166 Left exn -> liftIO $ exn @?= NoSuchBucket
160− _ -> return ()
167+ _ -> return ()
161168
162169 outFile <- mkRandFile 0
163170 step "simple fGetObject works"
@@ -165,39 +172,61 @@ basicTests = funTestWithBucket "Basic tests" $
165172
166173 let unmodifiedTime = UTCTime (fromGregorian 2010 11 26) 69857
167174 step "fGetObject an object which is modified now but requesting as un-modified in past, check for exception"
168− resE <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
169− gooIfUnmodifiedSince = (Just unmodifiedTime)
170− }
175+ resE <-
176+ try $
177+ fGetObject
178+ bucket
179+ "lsb-release"
180+ outFile
181+ defaultGetObjectOptions
182+ { gooIfUnmodifiedSince = (Just unmodifiedTime)
183+ }
171184 case resE of
172185 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
173− _ -> return ()
186+ _ -> return ()
174187
175188 step "fGetObject an object with no matching etag, check for exception"
176− resE1 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
177− gooIfMatch = (Just "invalid-etag")
178− }
189+ resE1 <-
190+ try $
191+ fGetObject
192+ bucket
193+ "lsb-release"
194+ outFile
195+ defaultGetObjectOptions
196+ { gooIfMatch = (Just "invalid-etag")
197+ }
179198 case resE1 of
180199 Left exn -> liftIO $ exn @?= ServiceErr "PreconditionFailed" "At least one of the pre-conditions you specified did not hold"
181− _ -> return ()
200+ _ -> return ()
182201
183202 step "fGetObject an object with no valid range, check for exception"
184− resE2 <- try $ fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
185− gooRange = (Just $ HT.ByteRangeFromTo 100 200)
186− }
203+ resE2 <-
204+ try $
205+ fGetObject
206+ bucket
207+ "lsb-release"
208+ outFile
209+ defaultGetObjectOptions
210+ { gooRange = (Just $ HT.ByteRangeFromTo 100 200)
211+ }
187212 case resE2 of
188− Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
189− _ -> return ()
213+ Left exn -> liftIO $ exn @?= ServiceErr "InvalidRange" "The requested range is not satisfiable"
214+ _ -> return ()
190215
191216 step "fGetObject on object with a valid range"
192− fGetObject bucket "lsb-release" outFile defaultGetObjectOptions {
193− gooRange = (Just $ HT.ByteRangeFrom 1)
194− }
217+ fGetObject
218+ bucket
219+ "lsb-release"
220+ outFile
221+ defaultGetObjectOptions
222+ { gooRange = (Just $ HT.ByteRangeFrom 1)
223+ }
195224
196225 step "fGetObject a non-existent object and check for NoSuchKey exception"
197226 resE3 <- try $ fGetObject bucket "noSuchKey" outFile defaultGetObjectOptions
198227 case resE3 of
199228 Left exn -> liftIO $ exn @?= NoSuchKey
200− _ -> return ()
229+ _ -> return ()
201230
202231 step "create new multipart upload works"
203232 uid <- newMultipartUpload bucket "newmpupload" []
@@ -224,250 +253,297 @@ basicTests = funTestWithBucket "Basic tests" $
224253
225254lowLevelMultipartTest :: TestTree
226255lowLevelMultipartTest = funTestWithBucket "Low-level Multipart Test" $
227− \step bucket -> do
228− -- low-level multipart operation tests.
229− let object = "newmpupload"
230− mb15 = 15 * 1024 * 1024
256+ \step bucket -> do
257+ -- low-level multipart operation tests.
258+ let object = "newmpupload"
259+ mb15 = 15 * 1024 * 1024
231260
232− step "Prepare for low-level multipart tests."
233− step "create new multipart upload"
234− uid <- newMultipartUpload bucket object []
235− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
261+ step "Prepare for low-level multipart tests."
262+ step "create new multipart upload"
263+ uid <- newMultipartUpload bucket object []
264+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
236265
237− randFile <- mkRandFile mb15
266+ randFile <- mkRandFile mb15
238267
239− step "put object parts 1 of 1"
240− h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
241− partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
268+ step "put object parts 1 of 1"
269+ h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
270+ partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15
242271
243− step "complete multipart"
244− void $ completeMultipartUpload bucket object uid [partInfo]
272+ step "complete multipart"
273+ void $ completeMultipartUpload bucket object uid [partInfo]
245274
246− destFile <- mkRandFile 0
247− step "Retrieve the created object and check size"
248− fGetObject bucket object destFile defaultGetObjectOptions
249− gotSize <- withNewHandle destFile getFileSize
250− liftIO $ gotSize == Right (Just mb15) @?
251− "Wrong file size of put file after getting"
275+ destFile <- mkRandFile 0
276+ step "Retrieve the created object and check size"
277+ fGetObject bucket object destFile defaultGetObjectOptions
278+ gotSize <- withNewHandle destFile getFileSize
279+ liftIO $
280+ gotSize == Right (Just mb15)
281+ @? "Wrong file size of put file after getting"
252282
253− step "Cleanup actions"
254− removeObject bucket object
283+ step "Cleanup actions"
284+ removeObject bucket object
255285
256286putObjectSizeTest :: TestTree
257287putObjectSizeTest = funTestWithBucket "PutObject of conduit source with size" $
258288 \step bucket -> do
259− -- putObject test (conduit source, size specified)
260− let obj = "msingle"
261− mb1 = 1 * 1024 * 1024
289+ -- putObject test (conduit source, size specified)
290+ let obj = "msingle"
291+ mb1 = 1 * 1024 * 1024
262292
263− step "Prepare for putObject with from source with size."
264− rFile <- mkRandFile mb1
293+ step "Prepare for putObject with from source with size."
294+ rFile <- mkRandFile mb1
265295
266− step "Upload single file."
267− putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
296+ step "Upload single file."
297+ putObject bucket obj (CB.sourceFile rFile) (Just mb1) defaultPutObjectOptions
268298
269− step "Retrieve and verify file size"
270− destFile <- mkRandFile 0
271− fGetObject bucket obj destFile defaultGetObjectOptions
272− gotSize <- withNewHandle destFile getFileSize
273− liftIO $ gotSize == Right (Just mb1) @?
274− "Wrong file size of put file after getting"
299+ step "Retrieve and verify file size"
300+ destFile <- mkRandFile 0
301+ fGetObject bucket obj destFile defaultGetObjectOptions
302+ gotSize <- withNewHandle destFile getFileSize
303+ liftIO $
304+ gotSize == Right (Just mb1)
305+ @? "Wrong file size of put file after getting"
275306
276− step "Cleanup actions"
277− deleteObject bucket obj
307+ step "Cleanup actions"
308+ deleteObject bucket obj
278309
279310putObjectNoSizeTest :: TestTree
280311putObjectNoSizeTest = funTestWithBucket "PutObject of conduit source with no size" $
281312 \step bucket -> do
282− -- putObject test (conduit source, no size specified)
283− let obj = "mpart"
284− mb70 = 70 * 1024 * 1024
313+ -- putObject test (conduit source, no size specified)
314+ let obj = "mpart"
315+ mb70 = 70 * 1024 * 1024
285316
286− step "Prepare for putObject with from source without providing size."
287− rFile <- mkRandFile mb70
317+ step "Prepare for putObject with from source without providing size."
318+ rFile <- mkRandFile mb70
288319
289− step "Upload multipart file."
290− putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
320+ step "Upload multipart file."
321+ putObject bucket obj (CB.sourceFile rFile) Nothing defaultPutObjectOptions
291322
292− step "Retrieve and verify file size"
293− destFile <- mkRandFile 0
294− fGetObject bucket obj destFile defaultGetObjectOptions
295− gotSize <- withNewHandle destFile getFileSize
296− liftIO $ gotSize == Right (Just mb70) @?
297− "Wrong file size of put file after getting"
323+ step "Retrieve and verify file size"
324+ destFile <- mkRandFile 0
325+ fGetObject bucket obj destFile defaultGetObjectOptions
326+ gotSize <- withNewHandle destFile getFileSize
327+ liftIO $
328+ gotSize == Right (Just mb70)
329+ @? "Wrong file size of put file after getting"
298330
299− step "Cleanup actions"
300− deleteObject bucket obj
331+ step "Cleanup actions"
332+ deleteObject bucket obj
301333
302334highLevelListingTest :: TestTree
303335highLevelListingTest = funTestWithBucket "High-level listObjects Test" $
304336 \step bucket -> do
305− step "High-level listObjects Test"
306− step "put 3 objects"
307− let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
308− extractObjectsFromList os =
309− mapM (\t -> case t of
310− ListItemObject o -> Just $ oiObject o
311− _ -> Nothing) os
312− expectedNonRecList = ["o4", "dir/"]
313− extractObjectsAndDirsFromList os =
314− map (\t -> case t of
315− ListItemObject o -> oiObject o
316− ListItemPrefix d -> d) os
317−
318− forM_ expectedObjects $
319− \obj -> fPutObject bucket obj "/etc/lsb-release" defaultPutObjectOptions
320−
321− step "High-level listing of objects"
322− items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
323− liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
324− extractObjectsAndDirsFromList items
325−
326− step "High-level recursive listing of objects"
327− objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
328−
329− liftIO $ assertEqual "Objects match failed!"
330− (Just $ sort expectedObjects) $
331− extractObjectsFromList objects
332−
333− step "High-level listing of objects (version 1)"
334− itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
335− liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
336− extractObjectsAndDirsFromList itemsV1
337−
338− step "High-level recursive listing of objects (version 1)"
339− objectsV1 <- C.runConduit $ listObjectsV1 bucket Nothing True C..|
340− sinkList
341−
342− liftIO $ assertEqual "Objects match failed!"
343− (Just $ sort expectedObjects) $
344− extractObjectsFromList objectsV1
345−
346− let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
347− expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
348− step "High-level listing with prefix"
349− prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
350− liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
351− expectedPrefListing $ extractObjectsAndDirsFromList prefItems
352−
353− step "High-level listing with prefix recursive"
354− prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
355− liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
356− expectedPrefListingRec $ extractObjectsFromList prefItemsRec
357−
358− step "High-level listing with prefix (version 1)"
359− prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
360− liftIO $ assertEqual "Objects/Dirs under prefix match failed!"
361− expectedPrefListing $ extractObjectsAndDirsFromList prefItemsV1
362−
363− step "High-level listing with prefix recursive (version 1)"
364− prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
365− liftIO $ assertEqual "Objects/Dirs under prefix match recursive failed!"
366− expectedPrefListingRec $ extractObjectsFromList prefItemsRecV1
367−
368− step "Cleanup actions"
369− forM_ expectedObjects $
370− \obj -> removeObject bucket obj
371−
372− step "High-level listIncompleteUploads Test"
373− let object = "newmpupload"
374− step "create 10 multipart uploads"
375− forM_ [1..10::Int] $ \_ -> do
376− uid <- newMultipartUpload bucket object []
377− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
378−
379− step "High-level listing of incomplete multipart uploads"
380− uploads <- C.runConduit $
381− listIncompleteUploads bucket (Just "newmpupload") True C..|
382− sinkList
383− liftIO $ length uploads @?= 10
384−
385− step "cleanup"
386− forM_ uploads $ \(UploadInfo _ uid _ _) ->
387− abortMultipartUpload bucket object uid
388−
389− step "High-level listIncompleteParts Test"
390− let mb5 = 5 * 1024 * 1024
391−
392− step "create a multipart upload"
393− uid <- newMultipartUpload bucket object []
394− liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
337+ step "High-level listObjects Test"
338+ step "put 3 objects"
339+ let expectedObjects = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3", "o4"]
340+ extractObjectsFromList os =
341+ mapM
342+ ( \t -> case t of
343+ ListItemObject o -> Just $ oiObject o
344+ _ -> Nothing
345+ )
346+ os
347+ expectedNonRecList = ["o4", "dir/"]
348+ extractObjectsAndDirsFromList os =
349+ map
350+ ( \t -> case t of
351+ ListItemObject o -> oiObject o
352+ ListItemPrefix d -> d
353+ )
354+ os
355+
356+ forM_ expectedObjects $
357+ \obj -> fPutObject bucket obj "/etc/lsb-release" defaultPutObjectOptions
358+
359+ step "High-level listing of objects"
360+ items <- C.runConduit $ listObjects bucket Nothing False C..| sinkList
361+ liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
362+ extractObjectsAndDirsFromList items
363+
364+ step "High-level recursive listing of objects"
365+ objects <- C.runConduit $ listObjects bucket Nothing True C..| sinkList
366+
367+ liftIO
368+ $ assertEqual
369+ "Objects match failed!"
370+ (Just $ sort expectedObjects)
371+ $ extractObjectsFromList objects
372+
373+ step "High-level listing of objects (version 1)"
374+ itemsV1 <- C.runConduit $ listObjectsV1 bucket Nothing False C..| sinkList
375+ liftIO $ assertEqual "Objects/Dirs match failed!" expectedNonRecList $
376+ extractObjectsAndDirsFromList itemsV1
377+
378+ step "High-level recursive listing of objects (version 1)"
379+ objectsV1 <-
380+ C.runConduit $
381+ listObjectsV1 bucket Nothing True
382+ C..| sinkList
383+
384+ liftIO
385+ $ assertEqual
386+ "Objects match failed!"
387+ (Just $ sort expectedObjects)
388+ $ extractObjectsFromList objectsV1
389+
390+ let expectedPrefListing = ["dir/o1", "dir/dir1/", "dir/dir2/"]
391+ expectedPrefListingRec = Just ["dir/dir1/o2", "dir/dir2/o3", "dir/o1"]
392+ step "High-level listing with prefix"
393+ prefItems <- C.runConduit $ listObjects bucket (Just "dir/") False C..| sinkList
394+ liftIO
395+ $ assertEqual
396+ "Objects/Dirs under prefix match failed!"
397+ expectedPrefListing
398+ $ extractObjectsAndDirsFromList prefItems
399+
400+ step "High-level listing with prefix recursive"
401+ prefItemsRec <- C.runConduit $ listObjects bucket (Just "dir/") True C..| sinkList
402+ liftIO
403+ $ assertEqual
404+ "Objects/Dirs under prefix match recursive failed!"
405+ expectedPrefListingRec
406+ $ extractObjectsFromList prefItemsRec
407+
408+ step "High-level listing with prefix (version 1)"
409+ prefItemsV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") False C..| sinkList
410+ liftIO
411+ $ assertEqual
412+ "Objects/Dirs under prefix match failed!"
413+ expectedPrefListing
414+ $ extractObjectsAndDirsFromList prefItemsV1
415+
416+ step "High-level listing with prefix recursive (version 1)"
417+ prefItemsRecV1 <- C.runConduit $ listObjectsV1 bucket (Just "dir/") True C..| sinkList
418+ liftIO
419+ $ assertEqual
420+ "Objects/Dirs under prefix match recursive failed!"
421+ expectedPrefListingRec
422+ $ extractObjectsFromList prefItemsRecV1
395423
396− step "put object parts 1..10"
397− inputFile <- mkRandFile mb5
398− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
399− forM_ [1..10] $ \pnum ->
400− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
424+ step "Cleanup actions"
425+ forM_ expectedObjects $
426+ \obj -> removeObject bucket obj
427+
428+ step "High-level listIncompleteUploads Test"
429+ let object = "newmpupload"
430+ step "create 10 multipart uploads"
431+ forM_ [1 .. 10 :: Int] $ \_ -> do
432+ uid <- newMultipartUpload bucket object []
433+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
401434
402− step "fetch list parts"
403− incompleteParts <- C.runConduit $ listIncompleteParts bucket object uid
404− C..| sinkList
405− liftIO $ length incompleteParts @?= 10
435+ step "High-level listing of incomplete multipart uploads"
436+ uploads <-
437+ C.runConduit $
438+ listIncompleteUploads bucket (Just "newmpupload") True
439+ C..| sinkList
440+ liftIO $ length uploads @?= 10
406441
407− step "cleanup"
442+ step "cleanup"
443+ forM_ uploads $ \(UploadInfo _ uid _ _) ->
408444 abortMultipartUpload bucket object uid
409445
446+ step "High-level listIncompleteParts Test"
447+ let mb5 = 5 * 1024 * 1024
448+
449+ step "create a multipart upload"
450+ uid <- newMultipartUpload bucket object []
451+ liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
452+
453+ step "put object parts 1..10"
454+ inputFile <- mkRandFile mb5
455+ h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
456+ forM_ [1 .. 10] $ \pnum ->
457+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
458+
459+ step "fetch list parts"
460+ incompleteParts <-
461+ C.runConduit $
462+ listIncompleteParts bucket object uid
463+ C..| sinkList
464+ liftIO $ length incompleteParts @?= 10
465+
466+ step "cleanup"
467+ abortMultipartUpload bucket object uid
468+
410469listingTest :: TestTree
411470listingTest = funTestWithBucket "Listing Test" $ \step bucket -> do
412− step "listObjects' test"
413− step "put 10 objects"
414− let objects = (\s ->T.concat ["lsb-release", T.pack (show s)]) <$> [1..10::Int]
415−
416− forM_ [1..10::Int] $ \s ->
417− fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release" defaultPutObjectOptions
418−
419− step "Simple list"
420− res <- listObjects' bucket Nothing Nothing Nothing Nothing
421− let expectedObjects = sort objects
422− liftIO $ assertEqual "Objects match failed!" expectedObjects
423− (map oiObject $ lorObjects res)
424−
425− step "Simple list version 1"
426− resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
427− let expected = sort $ map (T.concat .
428− ("lsb-release":) .
429− (\x -> [x]) .
430− T.pack .
431− show) [1..10::Int]
432− liftIO $ assertEqual "Objects match failed!" expected
433− (map oiObject $ lorObjects' resV1)
434−
435− step "Cleanup actions"
436− forM_ objects $ \obj -> deleteObject bucket obj
437−
438− step "listIncompleteUploads' test"
439− step "create 10 multipart uploads"
440− let object = "newmpupload"
441− forM_ [1..10::Int] $ \_ -> do
442− uid <- newMultipartUpload bucket object []
443− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
444−
445− step "list incomplete multipart uploads"
446− incompleteUploads <- listIncompleteUploads' bucket (Just "newmpupload") Nothing
447− Nothing Nothing Nothing
448− liftIO $ (length $ lurUploads incompleteUploads) @?= 10
449−
450− step "cleanup"
451− forM_ (lurUploads incompleteUploads) $
452− \(_, uid, _) -> abortMultipartUpload bucket object uid
453−
454− step "Basic listIncompleteParts Test"
455− let mb5 = 5 * 1024 * 1024
456−
457− step "create a multipart upload"
458− uid <- newMultipartUpload bucket object []
459− liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
460−
461− step "put object parts 1..10"
462− inputFile <- mkRandFile mb5
463− h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
464− forM_ [1..10] $ \pnum ->
465− putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
471+ step "listObjects' test"
472+ step "put 10 objects"
473+ let objects = (\s -> T.concat ["lsb-release", T.pack (show s)]) <$> [1 .. 10 :: Int]
474+
475+ forM_ [1 .. 10 :: Int] $ \s ->
476+ fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release" defaultPutObjectOptions
477+
478+ step "Simple list"
479+ res <- listObjects' bucket Nothing Nothing Nothing Nothing
480+ let expectedObjects = sort objects
481+ liftIO $
482+ assertEqual
483+ "Objects match failed!"
484+ expectedObjects
485+ (map oiObject $ lorObjects res)
486+
487+ step "Simple list version 1"
488+ resV1 <- listObjectsV1' bucket Nothing Nothing Nothing Nothing
489+ let expected =
490+ sort $
491+ map
492+ ( T.concat
493+ . ("lsb-release" :)
494+ . (\x -> [x])
495+ . T.pack
496+ . show
497+ )
498+ [1 .. 10 :: Int]
499+ liftIO $
500+ assertEqual
501+ "Objects match failed!"
502+ expected
503+ (map oiObject $ lorObjects' resV1)
504+
505+ step "Cleanup actions"
506+ forM_ objects $ \obj -> deleteObject bucket obj
507+
508+ step "listIncompleteUploads' test"
509+ step "create 10 multipart uploads"
510+ let object = "newmpupload"
511+ forM_ [1 .. 10 :: Int] $ \_ -> do
512+ uid <- newMultipartUpload bucket object []
513+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
466514
467− step "fetch list parts"
468− listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
469− liftIO $ (length $ lprParts listPartsResult) @?= 10
470− abortMultipartUpload bucket object uid
515+ step "list incomplete multipart uploads"
516+ incompleteUploads <-
517+ listIncompleteUploads'
518+ bucket
519+ (Just "newmpupload")
520+ Nothing
521+ Nothing
522+ Nothing
523+ Nothing
524+ liftIO $ (length $ lurUploads incompleteUploads) @?= 10
525+
526+ step "cleanup"
527+ forM_ (lurUploads incompleteUploads) $
528+ \(_, uid, _) -> abortMultipartUpload bucket object uid
529+
530+ step "Basic listIncompleteParts Test"
531+ let mb5 = 5 * 1024 * 1024
532+
533+ step "create a multipart upload"
534+ uid <- newMultipartUpload bucket object []
535+ liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.")
536+
537+ step "put object parts 1..10"
538+ inputFile <- mkRandFile mb5
539+ h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode
540+ forM_ [1 .. 10] $ \pnum ->
541+ putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb5
542+
543+ step "fetch list parts"
544+ listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing
545+ liftIO $ (length $ lprParts listPartsResult) @?= 10
546+ abortMultipartUpload bucket object uid
471547
472548presignedUrlFunTest :: TestTree
473549presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
@@ -479,29 +555,46 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
479555 mgr <- liftIO $ NC.newManager NC.tlsManagerSettings
480556
481557 step "PUT object presigned URL - makePresignedUrl"
482− putUrl <- makePresignedUrl 3600 HT.methodPut (Just bucket)
483− (Just obj) (Just "us-east-1") [] []
558+ putUrl <-
559+ makePresignedUrl
560+ 3600
561+ HT.methodPut
562+ (Just bucket)
563+ (Just obj)
564+ (Just "us-east-1")
565+ []
566+ []
484567
485568 let size1 = 1000 :: Int64
486569 inputFile <- mkRandFile size1
487570
488571 -- attempt to upload using the presigned URL
489572 putResp <- putR size1 inputFile mgr putUrl
490− liftIO $ (NC.responseStatus putResp == HT.status200) @?
491− "presigned PUT failed"
573+ liftIO $
574+ (NC.responseStatus putResp == HT.status200)
575+ @? "presigned PUT failed"
492576
493577 step "GET object presigned URL - makePresignedUrl"
494− getUrl <- makePresignedUrl 3600 HT.methodGet (Just bucket)
495− (Just obj) (Just "us-east-1") [] []
578+ getUrl <-
579+ makePresignedUrl
580+ 3600
581+ HT.methodGet
582+ (Just bucket)
583+ (Just obj)
584+ (Just "us-east-1")
585+ []
586+ []
496587
497588 getResp <- getR mgr getUrl
498− liftIO $ (NC.responseStatus getResp == HT.status200) @?
499− "presigned GET failed"
589+ liftIO $
590+ (NC.responseStatus getResp == HT.status200)
591+ @? "presigned GET failed"
500592
501593 -- read content from file to compare with response above
502594 bs <- C.runConduit $ CB.sourceFile inputFile C..| CB.sinkLbs
503− liftIO $ (bs == NC.responseBody getResp) @?
504− "presigned put and get got mismatched data"
595+ liftIO $
596+ (bs == NC.responseBody getResp)
597+ @? "presigned put and get got mismatched data"
505598
506599 step "PUT object presigned - presignedPutObjectURL"
507600 putUrl2 <- presignedPutObjectUrl bucket obj2 604800 []
@@ -510,66 +603,71 @@ presignedUrlFunTest = funTestWithBucket "presigned Url tests" $
510603 testFile <- mkRandFile size2
511604
512605 putResp2 <- putR size2 testFile mgr putUrl2
513− liftIO $ (NC.responseStatus putResp2 == HT.status200) @?
514− "presigned PUT failed (presignedPutObjectUrl)"
606+ liftIO $
607+ (NC.responseStatus putResp2 == HT.status200)
608+ @? "presigned PUT failed (presignedPutObjectUrl)"
515609
516610 step "HEAD object presigned URL - presignedHeadObjectUrl"
517611 headUrl <- presignedHeadObjectUrl bucket obj2 3600 []
518612
519− headResp <- do let req = NC.parseRequest_ $ toS headUrl
520− NC.httpLbs (req {NC.method = HT.methodHead}) mgr
521− liftIO $ (NC.responseStatus headResp == HT.status200) @?
522− "presigned HEAD failed (presignedHeadObjectUrl)"
613+ headResp <- do
614+ let req = NC.parseRequest_ $ toS $ decodeUtf8 headUrl
615+ NC.httpLbs (req {NC.method = HT.methodHead}) mgr
616+ liftIO $
617+ (NC.responseStatus headResp == HT.status200)
618+ @? "presigned HEAD failed (presignedHeadObjectUrl)"
523619
524620 -- check that header info is accurate
525621 let h = H.fromList $ NC.responseHeaders headResp
526622 cLen = H.lookupDefault "0" HT.hContentLength h
527− liftIO $ (cLen == show size2) @? "Head req returned bad content length"
623+ liftIO $ (cLen == showBS size2) @? "Head req returned bad content length"
528624
529625 step "GET object presigned URL - presignedGetObjectUrl"
530626 getUrl2 <- presignedGetObjectUrl bucket obj2 3600 [] []
531627
532628 getResp2 <- getR mgr getUrl2
533− liftIO $ (NC.responseStatus getResp2 == HT.status200) @?
534− "presigned GET failed (presignedGetObjectUrl)"
629+ liftIO $
630+ (NC.responseStatus getResp2 == HT.status200)
631+ @? "presigned GET failed (presignedGetObjectUrl)"
535632
536633 -- read content from file to compare with response above
537634 bs2 <- C.runConduit $ CB.sourceFile testFile C..| CB.sinkLbs
538− liftIO $ (bs2 == NC.responseBody getResp2) @?
539− "presigned put and get got mismatched data (presigned*Url)"
540−
635+ liftIO $
636+ (bs2 == NC.responseBody getResp2)
637+ @? "presigned put and get got mismatched data (presigned*Url)"
541638
542639 mapM_ (removeObject bucket) [obj, obj2]
543640 where
544641 putR size filePath mgr url = do
545− let req = NC.parseRequest_ $ toS url
546− let req' = req { NC.method = HT.methodPut
547− , NC.requestBody = NC.requestBodySource size $
548− CB.sourceFile filePath}
642+ let req = NC.parseRequest_ $ toS $ decodeUtf8 url
643+ let req' =
644+ req
645+ { NC.method = HT.methodPut,
646+ NC.requestBody =
647+ NC.requestBodySource size $
648+ CB.sourceFile filePath
649+ }
549650 NC.httpLbs req' mgr
550−
551651 getR mgr url = do
552− let req = NC.parseRequest_ $ toS url
652+ let req = NC.parseRequest_ $ toS $ decodeUtf8 url
553653 NC.httpLbs req mgr
554654
555655presignedPostPolicyFunTest :: TestTree
556656presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
557657 \step bucket -> do
558−
559658 step "presignedPostPolicy basic test"
560659 now <- liftIO $ Time.getCurrentTime
561660
562661 let key = "presignedPostPolicyTest/myfile"
563− policyConds = [ ppCondBucket bucket
564− , ppCondKey key
565− , ppCondContentLengthRange 1 1000
566− , ppCondContentType "application/octet-stream"
567− , ppCondSuccessActionStatus 200
568− ]
569−
662+ policyConds =
663+ [ ppCondBucket bucket,
664+ ppCondKey key,
665+ ppCondContentLengthRange 1 1000,
666+ ppCondContentType "application/octet-stream",
667+ ppCondSuccessActionStatus 200
668+ ]
570669 expirationTime = Time.addUTCTime 3600 now
571670 postPolicyE = newPostPolicy expirationTime policyConds
572−
573671 size = 1000 :: Int64
574672
575673 inputFile <- mkRandFile size
@@ -580,35 +678,35 @@ presignedPostPolicyFunTest = funTestWithBucket "Presigned Post Policy tests" $
580678 (url, formData) <- presignedPostPolicy postPolicy
581679 -- liftIO (print url) >> liftIO (print formData)
582680 result <- liftIO $ postForm url formData inputFile
583− liftIO $ (NC.responseStatus result == HT.status200) @?
584− "presigned POST failed"
681+ liftIO $
682+ (NC.responseStatus result == HT.status200)
683+ @? "presigned POST failed"
585684
586685 mapM_ (removeObject bucket) [key]
587− where
588−
589− postForm url formData inputFile = do
590− req <- NC.parseRequest $ toS url
591− let parts = map (\(x, y) -> Form.partBS x y) $
592− H.toList formData
593− parts' = parts ++ [Form.partFile "file" inputFile]
594− req' <- Form.formDataBody parts' req
595− mgr <- NC.newManager NC.tlsManagerSettings
596− NC.httpLbs req' mgr
686+ where
687+ postForm url formData inputFile = do
688+ req <- NC.parseRequest $ toS $ decodeUtf8 url
689+ let parts =
690+ map (\(x, y) -> Form.partBS x y) $
691+ H.toList formData
692+ parts' = parts ++ [Form.partFile "file" inputFile]
693+ req' <- Form.formDataBody parts' req
694+ mgr <- NC.newManager NC.tlsManagerSettings
695+ NC.httpLbs req' mgr
597696
598697bucketPolicyFunTest :: TestTree
599698bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
600699 \step bucket -> do
601−
602700 step "bucketPolicy basic test - no policy exception"
603701 resE <- try $ getBucketPolicy bucket
604702 case resE of
605703 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
606− _ -> return ()
704+ _ -> return ()
607705
608706 resE' <- try $ setBucketPolicy bucket T.empty
609707 case resE' of
610708 Left exn -> liftIO $ exn @?= ServiceErr "NoSuchBucketPolicy" "The bucket policy does not exist"
611− _ -> return ()
709+ _ -> return ()
612710
613711 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/*\"]}]}"
614712
@@ -616,7 +714,7 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
616714 resE'' <- try $ setBucketPolicy bucket expectedPolicyJSON
617715 case resE'' of
618716 Left exn -> liftIO $ exn @?= ServiceErr "MalformedPolicy" "bucket name does not match"
619− _ -> return ()
717+ _ -> return ()
620718
621719 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 <> "/*\"]}]}"
622720
@@ -631,13 +729,22 @@ bucketPolicyFunTest = funTestWithBucket "Bucket Policy tests" $
631729 step "verify bucket policy: (2) get `myobject` anonymously"
632730 connInfo <- asks mcConnInfo
633731 let proto = bool "http://" "https://" $ connectIsSecure connInfo
634− url = BS.concat [proto, getHostAddr connInfo, "/", toS bucket,
635− "/", toS obj]
636− respE <- liftIO $ (fmap (Right . toS) $ NC.simpleHttp $ toS url) `catch`
637− (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
732+ url =
733+ BS.concat
734+ [ proto,
735+ getHostAddr connInfo,
736+ "/",
737+ toUtf8 bucket,
738+ "/",
739+ toUtf8 obj
740+ ]
741+ respE <-
742+ liftIO $
743+ (fmap (Right . toStrictBS) $ NC.simpleHttp $ toS $ decodeUtf8 url)
744+ `catch` (\(e :: NC.HttpException) -> return $ Left (show e :: Text))
638745 case respE of
639− Left err -> liftIO $ assertFailure $ show err
640− Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")
746+ Left err -> liftIO $ assertFailure $ show err
747+ Right s -> liftIO $ s @?= (BS.concat $ replicate 100 "c")
641748
642749 deleteObject bucket obj
643750
@@ -658,8 +765,9 @@ multipartTest = funTestWithBucket "Multipart Tests" $
658765 destFile <- mkRandFile 0
659766 fGetObject bucket obj destFile defaultGetObjectOptions
660767 gotSize <- withNewHandle destFile getFileSize
661− liftIO $ gotSize == Right (Just mb80) @?
662− "Wrong file size of put file after getting"
768+ liftIO $
769+ gotSize == Right (Just mb80)
770+ @? "Wrong file size of put file after getting"
663771
664772 step "Cleanup actions"
665773 removeObject bucket obj
@@ -679,14 +787,16 @@ multipartTest = funTestWithBucket "Multipart Tests" $
679787 randFile <- mkRandFile kb5
680788
681789 step "upload 2 parts"
682− forM_ [1,2] $ \partNum -> do
790+ forM_ [1, 2] $ \partNum -> do
683791 h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode
684792 void $ putObjectPart bucket object uid partNum [] $ PayloadH h 0 kb5
685793
686794 step "remove ongoing upload"
687795 removeIncompleteUpload bucket object
688− uploads <- C.runConduit $ listIncompleteUploads bucket (Just object) False
689− C..| sinkList
796+ uploads <-
797+ C.runConduit $
798+ listIncompleteUploads bucket (Just object) False
799+ C..| sinkList
690800 liftIO $ (null uploads) @? "removeIncompleteUploads didn't complete successfully"
691801
692802putObjectContentTypeTest :: TestTree
@@ -698,9 +808,13 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
698808
699809 step "create server object with content-type"
700810 inputFile <- mkRandFile size1
701− fPutObject bucket object inputFile defaultPutObjectOptions {
702− pooContentType = Just "application/javascript"
703− }
811+ fPutObject
812+ bucket
813+ object
814+ inputFile
815+ defaultPutObjectOptions
816+ { pooContentType = Just "application/javascript"
817+ }
704818
705819 -- retrieve obj info to check
706820 oi <- headObject bucket object []
@@ -710,16 +824,23 @@ putObjectContentTypeTest = funTestWithBucket "putObject contentType tests" $
710824 liftIO $ assertEqual "Content-Type did not match" (Just "application/javascript") (H.lookup "Content-Type" m)
711825
712826 step "upload object with content-encoding set to identity"
713− fPutObject bucket object inputFile defaultPutObjectOptions {
714− pooContentEncoding = Just "identity"
715− }
827+ fPutObject
828+ bucket
829+ object
830+ inputFile
831+ defaultPutObjectOptions
832+ { pooContentEncoding = Just "identity"
833+ }
716834
717835 oiCE <- headObject bucket object []
718836 let m' = oiMetadata oiCE
719837
720838 step "Validate content-encoding"
721− liftIO $ assertEqual "Content-Encoding did not match" (Just "identity")
722− (H.lookup "Content-Encoding" m')
839+ liftIO $
840+ assertEqual
841+ "Content-Encoding did not match"
842+ (Just "identity")
843+ (H.lookup "Content-Encoding" m')
723844
724845 step "Cleanup actions"
725846
@@ -734,17 +855,24 @@ putObjectContentLanguageTest = funTestWithBucket "putObject contentLanguage test
734855
735856 step "create server object with content-language"
736857 inputFile <- mkRandFile size1
737− fPutObject bucket object inputFile defaultPutObjectOptions {
738− pooContentLanguage = Just "en-US"
739− }
858+ fPutObject
859+ bucket
860+ object
861+ inputFile
862+ defaultPutObjectOptions
863+ { pooContentLanguage = Just "en-US"
864+ }
740865
741866 -- retrieve obj info to check
742867 oi <- headObject bucket object []
743868 let m = oiMetadata oi
744869
745870 step "Validate content-language"
746− liftIO $ assertEqual "content-language did not match" (Just "en-US")
747− (H.lookup "Content-Language" m)
871+ liftIO $
872+ assertEqual
873+ "content-language did not match"
874+ (Just "en-US")
875+ (H.lookup "Content-Language" m)
748876 step "Cleanup actions"
749877
750878 removeObject bucket object
@@ -759,19 +887,25 @@ putObjectUserMetadataTest = funTestWithBucket "putObject user-metadata test" $
759887 step "create server object with usermetdata"
760888 inputFile <- mkRandFile size1
761889
762− fPutObject bucket object inputFile defaultPutObjectOptions {
763− pooUserMetadata = [ ("x-Amz-meta-mykey1", "myval1")
764− , ("mykey2", "myval2")
765− ]
766− }
890+ fPutObject
891+ bucket
892+ object
893+ inputFile
894+ defaultPutObjectOptions
895+ { pooUserMetadata =
896+ [ ("x-Amz-meta-mykey1", "myval1"),
897+ ("mykey2", "myval2")
898+ ]
899+ }
767900
768901 step "Validate user-metadata"
769902 -- retrieve obj info to check
770903 oi <- headObject bucket object []
771904 let m = oiUserMetadata oi
772905 -- need to do a case-insensitive comparison
773− sortedMeta = sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
774− H.toList m
906+ sortedMeta =
907+ sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
908+ H.toList m
775909 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
776910
777911 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
@@ -787,19 +921,25 @@ getObjectTest = funTestWithBucket "getObject test" $
787921 size1 = 100 :: Int64
788922
789923 inputFile <- mkRandFile size1
790− fPutObject bucket object inputFile defaultPutObjectOptions {
791− pooUserMetadata = [ ("x-Amz-meta-mykey1", "myval1")
792− , ("mykey2", "myval2")
793− ]
794− }
924+ fPutObject
925+ bucket
926+ object
927+ inputFile
928+ defaultPutObjectOptions
929+ { pooUserMetadata =
930+ [ ("x-Amz-meta-mykey1", "myval1"),
931+ ("mykey2", "myval2")
932+ ]
933+ }
795934
796935 step "get the object - check the metadata matches"
797936 -- retrieve obj info to check
798937 gor <- getObject bucket object defaultGetObjectOptions
799938 let m = oiUserMetadata $ gorObjectInfo gor
800939 -- need to do a case-insensitive comparison
801− sortedMeta = sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
802− H.toList m
940+ sortedMeta =
941+ sort $ map (\(k, v) -> (T.toLower k, T.toLower v)) $
942+ H.toList m
803943 ref = sort [("mykey1", "myval1"), ("mykey2", "myval2")]
804944
805945 liftIO $ (sortedMeta == ref) @? "Metadata mismatch!"
@@ -827,13 +967,21 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
827967 inputFile' <- mkRandFile size1
828968 inputFile'' <- mkRandFile size0
829969
830− fPutObject bucket object inputFile defaultPutObjectOptions {
831− pooStorageClass = Just "STANDARD"
832− }
833−
834− fPutObject bucket object' inputFile' defaultPutObjectOptions {
835− pooStorageClass = Just "REDUCED_REDUNDANCY"
836− }
970+ fPutObject
971+ bucket
972+ object
973+ inputFile
974+ defaultPutObjectOptions
975+ { pooStorageClass = Just "STANDARD"
976+ }
977+
978+ fPutObject
979+ bucket
980+ object'
981+ inputFile'
982+ defaultPutObjectOptions
983+ { pooStorageClass = Just "REDUCED_REDUNDANCY"
984+ }
837985
838986 removeObject bucket object
839987
@@ -842,15 +990,24 @@ putObjectStorageClassTest = funTestWithBucket "putObject storageClass tests" $
842990 let m' = oiMetadata oi'
843991
844992 step "Validate x-amz-storage-class rrs"
845− liftIO $ assertEqual "storageClass did not match" (Just "REDUCED_REDUNDANCY")
846− (H.lookup "X-Amz-Storage-Class" m')
847−
848− fpE <- try $ fPutObject bucket object'' inputFile'' defaultPutObjectOptions {
849− pooStorageClass = Just "INVALID_STORAGE_CLASS"
850− }
993+ liftIO $
994+ assertEqual
995+ "storageClass did not match"
996+ (Just "REDUCED_REDUNDANCY")
997+ (H.lookup "X-Amz-Storage-Class" m')
998+
999+ fpE <-
1000+ try $
1001+ fPutObject
1002+ bucket
1003+ object''
1004+ inputFile''
1005+ defaultPutObjectOptions
1006+ { pooStorageClass = Just "INVALID_STORAGE_CLASS"
1007+ }
8511008 case fpE of
8521009 Left exn -> liftIO $ exn @?= ServiceErr "InvalidStorageClass" "Invalid storage class."
853− _ -> return ()
1010+ _ -> return ()
8541011
8551012 step "Cleanup actions"
8561013
@@ -869,7 +1026,7 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
8691026 fPutObject bucket object inputFile defaultPutObjectOptions
8701027
8711028 step "copy object"
872− let srcInfo = defaultSourceInfo { srcBucket = bucket, srcObject = object}
1029+ let srcInfo = defaultSourceInfo {srcBucket = bucket, srcObject = object}
8731030 (etag, modTime) <- copyObjectSingle bucket objCopy srcInfo []
8741031
8751032 -- retrieve obj info to check
@@ -880,8 +1037,9 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
8801037
8811038 let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0
8821039
883− liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @?
884− "Copied object did not match expected."
1040+ liftIO $
1041+ (s == size1 && e == etag && isMTimeDiffOk)
1042+ @? "Copied object did not match expected."
8851043
8861044 step "cleanup actions"
8871045 removeObject bucket object
@@ -902,12 +1060,18 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
9021060 liftIO $ (T.length uid > 0) @? "Got an empty multipartUpload Id."
9031061
9041062 step "put object parts 1-3"
905− let srcInfo' = defaultSourceInfo { srcBucket = bucket, srcObject = srcObj }
906− dstInfo' = defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj }
907− parts <- forM [1..3] $ \p -> do
908− (etag', _) <- copyObjectPart dstInfo' srcInfo'{
909− srcRange = Just $ (,) ((p-1)*mb5) ((p-1)*mb5 + (mb5 - 1))
910− } uid (fromIntegral p) []
1063+ let srcInfo' = defaultSourceInfo {srcBucket = bucket, srcObject = srcObj}
1064+ dstInfo' = defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1065+ parts <- forM [1 .. 3] $ \p -> do
1066+ (etag', _) <-
1067+ copyObjectPart
1068+ dstInfo'
1069+ srcInfo'
1070+ { srcRange = Just $ (,) ((p -1) * mb5) ((p -1) * mb5 + (mb5 - 1))
1071+ }
1072+ uid
1073+ (fromIntegral p)
1074+ []
9111075 return (fromIntegral p, etag')
9121076
9131077 step "complete multipart"
@@ -953,11 +1117,13 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
9531117 fPutObject bucket src inputFile' defaultPutObjectOptions
9541118
9551119 step "copy last 10MiB of object"
956− copyObject defaultDestinationInfo { dstBucket = bucket, dstObject = copyObj } defaultSourceInfo {
957− srcBucket = bucket
958− , srcObject = src
959− , srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
960− }
1120+ copyObject
1121+ defaultDestinationInfo {dstBucket = bucket, dstObject = copyObj}
1122+ defaultSourceInfo
1123+ { srcBucket = bucket,
1124+ srcObject = src,
1125+ srcRange = Just $ (,) (5 * 1024 * 1024) (size - 1)
1126+ }
9611127
9621128 step "verify uploaded object"
9631129 cSize <- oiSize <$> headObject bucket copyObj []
@@ -968,42 +1134,43 @@ copyObjectTests = funTestWithBucket "copyObject related tests" $
9681134
9691135getNPutSSECTest :: TestTree
9701136getNPutSSECTest =
971− funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
972− -- Skip this test if the server is not using TLS as encryption is
973− -- disabled anyway.
974− isTLSConn <- asks (connectIsSecure . mcConnInfo)
975− if isTLSConn
976− then do step "Make an encryption key"
977− key <- case mkSSECKey $ BS.pack [0..31] of
978− Nothing -> liftIO $ assertFailure "This should not happen"
979− Just k -> return k
980−
981− let mb1 = 1024*1024
982− obj = "1"
983− step "Upload an object using the encryption key"
984− rFile <- mkRandFile mb1
985− let putOpts = defaultPutObjectOptions { pooSSE = Just $ SSEC key }
986− fPutObject bucket obj rFile putOpts
987−
988− step "Stat object without key - should fail"
989− headRes <- try $ statObject bucket obj defaultGetObjectOptions
990− case headRes of
991− Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
992− Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
993− | NC.responseStatus rsp == HT.status400 -> return ()
994− | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
995− Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
996−
997− step "Get file and check length"
998− dstFile <- mkRandFile 0
999− let getOpts = defaultGetObjectOptions { gooSSECKey = Just key }
1000− fGetObject bucket obj dstFile getOpts
1001−
1002− gotSize <- withNewHandle dstFile getFileSize
1003− liftIO $ gotSize == Right (Just mb1) @?
1004− "Wrong file size of object when getting"
1005−
1006− step "Cleanup"
1007− deleteObject bucket obj
1008−
1009− else step "Skipping encryption test as server is not using TLS"
1137+ funTestWithBucket "Get and Put SSE-C Test" $ \step bucket -> do
1138+ -- Skip this test if the server is not using TLS as encryption is
1139+ -- disabled anyway.
1140+ isTLSConn <- asks (connectIsSecure . mcConnInfo)
1141+ if isTLSConn
1142+ then do
1143+ step "Make an encryption key"
1144+ key <- case mkSSECKey $ BS.pack [0 .. 31] of
1145+ Nothing -> liftIO $ assertFailure "This should not happen"
1146+ Just k -> return k
1147+
1148+ let mb1 = 1024 * 1024
1149+ obj = "1"
1150+ step "Upload an object using the encryption key"
1151+ rFile <- mkRandFile mb1
1152+ let putOpts = defaultPutObjectOptions {pooSSE = Just $ SSEC key}
1153+ fPutObject bucket obj rFile putOpts
1154+
1155+ step "Stat object without key - should fail"
1156+ headRes <- try $ statObject bucket obj defaultGetObjectOptions
1157+ case headRes of
1158+ Right _ -> liftIO $ assertFailure "Cannot perform head object on encrypted object without specifying key"
1159+ Left ex@(NC.HttpExceptionRequest _ (NC.StatusCodeException rsp _))
1160+ | NC.responseStatus rsp == HT.status400 -> return ()
1161+ | otherwise -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1162+ Left ex -> liftIO $ assertFailure $ "Unexpected err: " ++ show ex
1163+
1164+ step "Get file and check length"
1165+ dstFile <- mkRandFile 0
1166+ let getOpts = defaultGetObjectOptions {gooSSECKey = Just key}
1167+ fGetObject bucket obj dstFile getOpts
1168+
1169+ gotSize <- withNewHandle dstFile getFileSize
1170+ liftIO $
1171+ gotSize == Right (Just mb1)
1172+ @? "Wrong file size of object when getting"
1173+
1174+ step "Cleanup"
1175+ deleteObject bucket obj
1176+ else step "Skipping encryption test as server is not using TLS"
test/Network/Minio/API/Test.hs+73−61
@@ -15,88 +15,100 @@
1515--
1616
1717module Network.Minio.API.Test
18− ( bucketNameValidityTests
19− , objectNameValidityTests
20− , parseServerInfoJSONTest
21− , parseHealStatusTest
22− , parseHealStartRespTest
23− ) where
24−
25−import Data.Aeson (eitherDecode)
26−import Test.Tasty
27−import Test.Tasty.HUnit
28−
29−import Lib.Prelude
30−
31−import Network.Minio.AdminAPI
32−import Network.Minio.API
18+ ( bucketNameValidityTests,
19+ objectNameValidityTests,
20+ parseServerInfoJSONTest,
21+ parseHealStatusTest,
22+ parseHealStartRespTest,
23+ )
24+where
25+
26+import Data.Aeson (eitherDecode)
27+import Lib.Prelude
28+import Network.Minio.API
29+import Network.Minio.AdminAPI
30+import Test.Tasty
31+import Test.Tasty.HUnit
3332
3433assertBool' :: Bool -> Assertion
3534assertBool' = assertBool "Test failed!"
3635
3736bucketNameValidityTests :: TestTree
38−bucketNameValidityTests = testGroup "Bucket Name Validity Tests"
39− [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName ""
40− , testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab"
41− , testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
42− , testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD"
43− , testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2"
44− , testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-"
45− , testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg"
46− , testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1"
47− , testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea"
48− , testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d"
49− , testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
50− ]
37+bucketNameValidityTests =
38+ testGroup
39+ "Bucket Name Validity Tests"
40+ [ testCase "Too short 1" $ assertBool' $ not $ isValidBucketName "",
41+ testCase "Too short 2" $ assertBool' $ not $ isValidBucketName "ab",
42+ testCase "Too long 1" $ assertBool' $ not $ isValidBucketName "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
43+ testCase "Has upper case" $ assertBool' $ not $ isValidBucketName "ABCD",
44+ testCase "Has punctuation" $ assertBool' $ not $ isValidBucketName "abc,2",
45+ testCase "Has hyphen at end" $ assertBool' $ not $ isValidBucketName "abc-",
46+ testCase "Has consecutive dot" $ assertBool' $ not $ isValidBucketName "abck..eedg",
47+ testCase "Looks like IP" $ assertBool' $ not $ isValidBucketName "10.0.0.1",
48+ testCase "Valid bucket name 1" $ assertBool' $ isValidBucketName "abcd.pqeq.rea",
49+ testCase "Valid bucket name 2" $ assertBool' $ isValidBucketName "abcdedgh1d",
50+ testCase "Valid bucket name 3" $ assertBool' $ isValidBucketName "abc-de-dg-h1d"
51+ ]
5152
5253objectNameValidityTests :: TestTree
53−objectNameValidityTests = testGroup "Object Name Validity Tests"
54− [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName ""
55− , testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
56− ]
54+objectNameValidityTests =
55+ testGroup
56+ "Object Name Validity Tests"
57+ [ testCase "Empty name" $ assertBool' $ not $ isValidObjectName "",
58+ testCase "Has unicode characters" $ assertBool' $ isValidObjectName "日本国"
59+ ]
5760
5861parseServerInfoJSONTest :: TestTree
59−parseServerInfoJSONTest = testGroup "Parse MinIO Admin API ServerInfo JSON test" $
60− map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
61− tfn (eitherDecode tVal :: Either [Char] [ServerInfo])) testCases
62+parseServerInfoJSONTest =
63+ testGroup "Parse MinIO Admin API ServerInfo JSON test" $
64+ map
65+ ( \(tName, tDesc, tfn, tVal) ->
66+ testCase tName $ assertBool tDesc $
67+ tfn (eitherDecode tVal :: Either [Char] [ServerInfo])
68+ )
69+ testCases
6270 where
63− testCases = [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON)
64− , ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON)
65− , ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
66− ]
71+ testCases =
72+ [ ("FSBackend", "Verify server info json parsing for FS backend", isRight, fsJSON),
73+ ("Erasure Backend", "Verify server info json parsing for Erasure backend", isRight, erasureJSON),
74+ ("Unknown Backend", "Verify server info json parsing for invalid backend", isLeft, invalidJSON)
75+ ]
6776 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\":[]}}}]"
68−
6977 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\":[]}}}]"
70−
7178 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\":[]}}}]"
7279
7380parseHealStatusTest :: TestTree
74−parseHealStatusTest = testGroup "Parse MinIO Admin API HealStatus JSON test" $
75− map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
76− tfn (eitherDecode tVal :: Either [Char] HealStatus)) testCases
77−
81+parseHealStatusTest =
82+ testGroup "Parse MinIO Admin API HealStatus JSON test" $
83+ map
84+ ( \(tName, tDesc, tfn, tVal) ->
85+ testCase tName $ assertBool tDesc $
86+ tfn (eitherDecode tVal :: Either [Char] HealStatus)
87+ )
88+ testCases
7889 where
79− testCases = [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON')
80− , ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON')
81− , ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
82− ]
83−
90+ testCases =
91+ [ ("Good", "Verify heal result item for erasure backend", isRight, erasureJSON'),
92+ ("Corrupted", "Verify heal result item for erasure backend", isLeft, invalidJSON'),
93+ ("Incorrect Value", "Verify heal result item for erasure backend", isLeft, invalidItemType)
94+ ]
8495 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}]}"
85−
8696 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}]"
87−
8897 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}]}"
8998
9099parseHealStartRespTest :: TestTree
91−parseHealStartRespTest = testGroup "Parse MinIO Admin API HealStartResp JSON test" $
92− map (\(tName, tDesc, tfn, tVal) -> testCase tName $ assertBool tDesc $
93− tfn (eitherDecode tVal :: Either [Char] HealStartResp)) testCases
94−
100+parseHealStartRespTest =
101+ testGroup "Parse MinIO Admin API HealStartResp JSON test" $
102+ map
103+ ( \(tName, tDesc, tfn, tVal) ->
104+ testCase tName $ assertBool tDesc $
105+ tfn (eitherDecode tVal :: Either [Char] HealStartResp)
106+ )
107+ testCases
95108 where
96− testCases = [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON)
97− , ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
98− ]
99−
109+ testCases =
110+ [ ("Good", "Verify heal start response for erasure backend", isRight, hsrJSON),
111+ ("Missing Token", "Verify heal start response for erasure backend", isLeft, missingTokenJSON)
112+ ]
100113 hsrJSON = "{\"clientToken\":\"3a3aca49-77dd-4b78-bba7-0978f119b23e\",\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
101−
102114 missingTokenJSON = "{\"clientAddress\":\"127.0.0.1\",\"startTime\":\"2018-06-05T08:09:47.644394493Z\"}"
test/Network/Minio/JsonParser/Test.hs+26−27
@@ -15,23 +15,23 @@
1515--
1616
1717module Network.Minio.JsonParser.Test
18− (
19− jsonParserTests
20− ) where
18+ ( jsonParserTests,
19+ )
20+where
2121
22−import Test.Tasty
23−import Test.Tasty.HUnit
24−import UnliftIO (MonadUnliftIO)
25−
26−import Lib.Prelude
27−
28−import Network.Minio.Errors
29−import Network.Minio.JsonParser
22+import Lib.Prelude
23+import Network.Minio.Errors
24+import Network.Minio.JsonParser
25+import Test.Tasty
26+import Test.Tasty.HUnit
27+import UnliftIO (MonadUnliftIO)
3028
3129jsonParserTests :: TestTree
32−jsonParserTests = testGroup "JSON Parser Tests"
33− [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34− ]
30+jsonParserTests =
31+ testGroup
32+ "JSON Parser Tests"
33+ [ testCase "Test parseErrResponseJSON" testParseErrResponseJSON
34+ ]
3535
3636tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
3737tryValidationErr act = try act
@@ -43,22 +43,21 @@ testParseErrResponseJSON :: Assertion
4343testParseErrResponseJSON = do
4444 -- 1. Test parsing of an invalid error json.
4545 parseResE <- tryValidationErr $ parseErrResponseJSON "ClearlyInvalidJSON"
46− when (isRight parseResE) $
47− assertFailure $ "Parsing should have failed => " ++ show parseResE
46+ when (isRight parseResE)
47+ $ assertFailure
48+ $ "Parsing should have failed => " ++ show parseResE
4849
4950 forM_ cases $ \(jsondata, sErr) -> do
5051 parseErr <- tryValidationErr $ parseErrResponseJSON jsondata
5152 either assertValidationErr (@?= sErr) parseErr
52−
5353 where
54− cases = [
55− -- 2. Test parsing of a valid error json.
56− ("{\"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\"}",
57− ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58− )
59− ,
60− -- 3. Test parsing of a valid, empty Resource.
61− ("{\"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\"}",
62− ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
63− )
54+ cases =
55+ [ -- 2. Test parsing of a valid error json.
56+ ( "{\"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\"}",
57+ ServiceErr "InvalidAccessKeyId" "The access key ID you provided does not exist in our records."
58+ ),
59+ -- 3. Test parsing of a valid, empty Resource.
60+ ( "{\"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\"}",
61+ ServiceErr "SignatureDoesNotMatch" "The request signature we calculated does not match the signature you provided. Check your key and signing method."
62+ )
6463 ]
test/Network/Minio/TestHelpers.hs+9−8
@@ -15,18 +15,19 @@
1515--
1616
1717module Network.Minio.TestHelpers
18− ( runTestNS
19− ) where
18+ ( runTestNS,
19+ )
20+where
2021
21−import Network.Minio.Data
22+import Lib.Prelude
23+import Network.Minio.Data
2224
23−import Lib.Prelude
24−
25−newtype TestNS = TestNS { testNamespace :: Text }
25+newtype TestNS = TestNS {testNamespace :: Text}
2626
2727instance HasSvcNamespace TestNS where
2828 getSvcNamespace = testNamespace
2929
3030runTestNS :: ReaderT TestNS m a -> m a
31−runTestNS = flip runReaderT $
32− TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
31+runTestNS =
32+ flip runReaderT $
33+ TestNS "http://s3.amazonaws.com/doc/2006-03-01/"
test/Network/Minio/Utils/Test.hs+16−17
@@ -15,33 +15,32 @@
1515--
1616
1717module Network.Minio.Utils.Test
18− (
19− limitedMapConcurrentlyTests
20− ) where
18+ ( limitedMapConcurrentlyTests,
19+ )
20+where
2121
22−import Test.Tasty
23−import Test.Tasty.HUnit
24−
25−import Lib.Prelude
26−
27−import Network.Minio.Utils
22+import Lib.Prelude
23+import Network.Minio.Utils
24+import Test.Tasty
25+import Test.Tasty.HUnit
2826
2927limitedMapConcurrentlyTests :: TestTree
30−limitedMapConcurrentlyTests = testGroup "limitedMapConcurrently Tests"
31− [ testCase "Test with various thread counts" testLMC
32− ]
28+limitedMapConcurrentlyTests =
29+ testGroup
30+ "limitedMapConcurrently Tests"
31+ [ testCase "Test with various thread counts" testLMC
32+ ]
3333
3434testLMC :: Assertion
3535testLMC = do
3636 let maxNum = 50
3737 -- test with thread count of 1 to 2*maxNum
38− forM_ [1..(2*maxNum)] $ \threads -> do
39− res <- limitedMapConcurrently threads compute [1..maxNum]
38+ forM_ [1 .. (2 * maxNum)] $ \threads -> do
39+ res <- limitedMapConcurrently threads compute [1 .. maxNum]
4040 sum res @?= overallResultCheck maxNum
4141 where
4242 -- simple function to run in each thread
4343 compute :: Int -> IO Int
44− compute n = return $ sum [1..n]
45−
44+ compute n = return $ sum [1 .. n]
4645 -- function to check overall result
47− overallResultCheck n = sum $ map (\t -> (t * (t+1)) `div` 2) [1..n]
46+ overallResultCheck n = sum $ map (\t -> (t * (t + 1)) `div` 2) [1 .. n]
test/Network/Minio/XmlGenerator/Test.hs+124−94
@@ -13,30 +13,31 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−
1716{-# LANGUAGE QuasiQuotes #-}
18−module Network.Minio.XmlGenerator.Test
19− ( xmlGeneratorTests
20− ) where
21−
22−import Test.Tasty
23−import Test.Tasty.HUnit
24−import Text.RawString.QQ (r)
2517
26−import Lib.Prelude
18+module Network.Minio.XmlGenerator.Test
19+ ( xmlGeneratorTests,
20+ )
21+where
2722
28−import Network.Minio.Data
29−import Network.Minio.TestHelpers
30−import Network.Minio.XmlGenerator
31−import Network.Minio.XmlParser (parseNotification)
23+import Lib.Prelude
24+import Network.Minio.Data
25+import Network.Minio.TestHelpers
26+import Network.Minio.XmlGenerator
27+import Network.Minio.XmlParser (parseNotification)
28+import Test.Tasty
29+import Test.Tasty.HUnit
30+import Text.RawString.QQ (r)
3231
3332xmlGeneratorTests :: TestTree
34−xmlGeneratorTests = testGroup "XML Generator Tests"
35− [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig
36− , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest
37− , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest
38− , testCase "Test mkSelectRequest" testMkSelectRequest
39− ]
33+xmlGeneratorTests =
34+ testGroup
35+ "XML Generator Tests"
36+ [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig,
37+ testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest,
38+ testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest,
39+ testCase "Test mkSelectRequest" testMkSelectRequest
40+ ]
4041
4142testMkCreateBucketConfig :: Assertion
4243testMkCreateBucketConfig = do
@@ -44,100 +45,129 @@ testMkCreateBucketConfig = do
4445 assertEqual "CreateBucketConfiguration xml should match: " expected $
4546 mkCreateBucketConfig ns "EU"
4647 where
47− expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
48− \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
49− \<LocationConstraint>EU</LocationConstraint>\
50− \</CreateBucketConfiguration>"
48+ expected =
49+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
50+ \<CreateBucketConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
51+ \<LocationConstraint>EU</LocationConstraint>\
52+ \</CreateBucketConfiguration>"
5153
5254testMkCompleteMultipartUploadRequest :: Assertion
5355testMkCompleteMultipartUploadRequest =
5456 assertEqual "completeMultipartUpload xml should match: " expected $
55− mkCompleteMultipartUploadRequest [(1, "abc")]
57+ mkCompleteMultipartUploadRequest [(1, "abc")]
5658 where
57− expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
58− \<CompleteMultipartUpload>\
59− \<Part>\
60− \<PartNumber>1</PartNumber><ETag>abc</ETag>\
61− \</Part>\
62− \</CompleteMultipartUpload>"
63−
59+ expected =
60+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
61+ \<CompleteMultipartUpload>\
62+ \<Part>\
63+ \<PartNumber>1</PartNumber><ETag>abc</ETag>\
64+ \</Part>\
65+ \</CompleteMultipartUpload>"
6466
6567testMkPutNotificationRequest :: Assertion
6668testMkPutNotificationRequest =
6769 forM_ cases $ \val -> do
6870 let ns = "http://s3.amazonaws.com/doc/2006-03-01/"
69− result = toS $ mkPutNotificationRequest ns val
71+ result = fromStrictBS $ mkPutNotificationRequest ns val
7072 ntf <- runExceptT $ runTestNS $ parseNotification result
71− either (\_ -> assertFailure "XML Parse Error!")
72− (@?= val) ntf
73+ either
74+ (\_ -> assertFailure "XML Parse Error!")
75+ (@?= val)
76+ ntf
7377 where
74− cases = [ Notification []
75− [ NotificationConfig
76− "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
77− "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
78− [ReducedRedundancyLostObject, ObjectCreated] defaultFilter
79− ]
80− []
81− , Notification
82− [ NotificationConfig
83− "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
84− [ObjectCreatedPut]
85− (Filter $ FilterKey $ FilterRules
86− [ FilterRule "prefix" "images/"
87− , FilterRule "suffix" ".jpg"])
88− , NotificationConfig
89− "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
90− [ObjectCreated] defaultFilter
91− ]
92− [ NotificationConfig
93− "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
94− [ReducedRedundancyLostObject] defaultFilter
95− ]
96− [ NotificationConfig
97− "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
98− [ObjectCreated] defaultFilter
99− ]
100− ]
78+ cases =
79+ [ Notification
80+ []
81+ [ NotificationConfig
82+ "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
83+ "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
84+ [ReducedRedundancyLostObject, ObjectCreated]
85+ defaultFilter
86+ ]
87+ [],
88+ Notification
89+ [ NotificationConfig
90+ "1"
91+ "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
92+ [ObjectCreatedPut]
93+ ( Filter $ FilterKey $
94+ FilterRules
95+ [ FilterRule "prefix" "images/",
96+ FilterRule "suffix" ".jpg"
97+ ]
98+ ),
99+ NotificationConfig
100+ ""
101+ "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
102+ [ObjectCreated]
103+ defaultFilter
104+ ]
105+ [ NotificationConfig
106+ ""
107+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
108+ [ReducedRedundancyLostObject]
109+ defaultFilter
110+ ]
111+ [ NotificationConfig
112+ "ObjectCreatedEvents"
113+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
114+ [ObjectCreated]
115+ defaultFilter
116+ ]
117+ ]
101118
102119testMkSelectRequest :: Assertion
103120testMkSelectRequest = mapM_ assertFn cases
104121 where
105122 assertFn (a, b) = assertEqual "selectRequest XML should match: " b $ mkSelectRequest a
106− cases = [ ( SelectRequest "Select * from S3Object" SQL
107− (InputSerialization (Just CompressionTypeGzip)
108− (InputFormatCSV $ fileHeaderInfo FileHeaderIgnore
109− <> recordDelimiter "\n"
110− <> fieldDelimiter ","
111− <> quoteCharacter "\""
112− <> quoteEscapeCharacter "\""
113− ))
114− (OutputSerializationCSV $ quoteFields QuoteFieldsAsNeeded
115− <> recordDelimiter "\n"
116− <> fieldDelimiter ","
117− <> quoteCharacter "\""
118− <> quoteEscapeCharacter "\""
123+ cases =
124+ [ ( SelectRequest
125+ "Select * from S3Object"
126+ SQL
127+ ( InputSerialization
128+ (Just CompressionTypeGzip)
129+ ( InputFormatCSV $
130+ fileHeaderInfo FileHeaderIgnore
131+ <> recordDelimiter "\n"
132+ <> fieldDelimiter ","
133+ <> quoteCharacter "\""
134+ <> quoteEscapeCharacter "\""
119135 )
120− (Just False)
121− , [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><QuoteCharacter>&#34;</QuoteCharacter><RecordDelimiter>
136+ )
137+ ( OutputSerializationCSV $
138+ quoteFields QuoteFieldsAsNeeded
139+ <> recordDelimiter "\n"
140+ <> fieldDelimiter ","
141+ <> quoteCharacter "\""
142+ <> quoteEscapeCharacter "\""
143+ )
144+ (Just False),
145+ [r|<?xml version="1.0" encoding="UTF-8"?><SelectRequest><Expression>Select * from S3Object</Expression><ExpressionType>SQL</ExpressionType><InputSerialization><CompressionType>GZIP</CompressionType><CSV><QuoteCharacter>&#34;</QuoteCharacter><RecordDelimiter>
122146</RecordDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
123147</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
124− )
125− , ( setRequestProgressEnabled False $
126− setInputCompressionType CompressionTypeGzip $
127− selectRequest "Select * from S3Object" documentJsonInput
128− (outputJSONFromRecordDelimiter "\n")
129− , [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>
148+ ),
149+ ( setRequestProgressEnabled False
150+ $ setInputCompressionType CompressionTypeGzip
151+ $ selectRequest
152+ "Select * from S3Object"
153+ documentJsonInput
154+ (outputJSONFromRecordDelimiter "\n"),
155+ [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>
130156</RecordDelimiter></JSON></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
131− )
132− , ( setRequestProgressEnabled False $
133− setInputCompressionType CompressionTypeNone $
134− selectRequest "Select * from S3Object" defaultParquetInput
135− (outputCSVFromProps $ quoteFields QuoteFieldsAsNeeded
136− <> recordDelimiter "\n"
137− <> fieldDelimiter ","
138− <> quoteCharacter "\""
139− <> quoteEscapeCharacter "\"")
140− , [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>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
157+ ),
158+ ( setRequestProgressEnabled False
159+ $ setInputCompressionType CompressionTypeNone
160+ $ selectRequest
161+ "Select * from S3Object"
162+ defaultParquetInput
163+ ( outputCSVFromProps $
164+ quoteFields QuoteFieldsAsNeeded
165+ <> recordDelimiter "\n"
166+ <> fieldDelimiter ","
167+ <> quoteCharacter "\""
168+ <> quoteEscapeCharacter "\""
169+ ),
170+ [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>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
141171</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
142− )
143− ]
172+ )
173+ ]
test/Network/Minio/XmlParser/Test.hs+297−276
@@ -13,39 +13,40 @@
1313-- See the License for the specific language governing permissions and
1414-- limitations under the License.
1515--
16−
1716{-# LANGUAGE QuasiQuotes #-}
18−module Network.Minio.XmlParser.Test
19− ( xmlParserTests
20− ) where
21−
22−import qualified Data.HashMap.Strict as H
23−import Data.Time (fromGregorian)
24−import Test.Tasty
25−import Test.Tasty.HUnit
26−import Text.RawString.QQ (r)
27−import UnliftIO (MonadUnliftIO)
2817
29−import Lib.Prelude
30−
31−import Network.Minio.Data
32−import Network.Minio.Errors
33−import Network.Minio.TestHelpers
34−import Network.Minio.XmlParser
18+module Network.Minio.XmlParser.Test
19+ ( xmlParserTests,
20+ )
21+where
22+
23+import qualified Data.HashMap.Strict as H
24+import Data.Time (fromGregorian)
25+import Lib.Prelude
26+import Network.Minio.Data
27+import Network.Minio.Errors
28+import Network.Minio.TestHelpers
29+import Network.Minio.XmlParser
30+import Test.Tasty
31+import Test.Tasty.HUnit
32+import Text.RawString.QQ (r)
33+import UnliftIO (MonadUnliftIO)
3534
3635xmlParserTests :: TestTree
37−xmlParserTests = testGroup "XML Parser Tests"
38− [ testCase "Test parseLocation" testParseLocation
39− , testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload
40− , testCase "Test parseListObjectsResponse" testParseListObjectsResult
41− , testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result
42− , testCase "Test parseListUploadsresponse" testParseListIncompleteUploads
43− , testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse
44− , testCase "Test parseListPartsResponse" testParseListPartsResponse
45− , testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse
46− , testCase "Test parseNotification" testParseNotification
47− , testCase "Test parseSelectProgress" testParseSelectProgress
48− ]
36+xmlParserTests =
37+ testGroup
38+ "XML Parser Tests"
39+ [ testCase "Test parseLocation" testParseLocation,
40+ testCase "Test parseNewMultipartUpload" testParseNewMultipartUpload,
41+ testCase "Test parseListObjectsResponse" testParseListObjectsResult,
42+ testCase "Test parseListObjectsV1Response" testParseListObjectsV1Result,
43+ testCase "Test parseListUploadsresponse" testParseListIncompleteUploads,
44+ testCase "Test parseCompleteMultipartUploadResponse" testParseCompleteMultipartUploadResponse,
45+ testCase "Test parseListPartsResponse" testParseListPartsResponse,
46+ testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse,
47+ testCase "Test parseNotification" testParseNotification,
48+ testCase "Test parseSelectProgress" testParseSelectProgress
49+ ]
4950
5051tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
5152tryValidationErr act = try act
@@ -54,232 +55,232 @@ assertValidtionErr :: MErrV -> Assertion
5455assertValidtionErr e = assertFailure $ "Failed due to validation error => " ++ show e
5556
5657eitherValidationErr :: Either MErrV a -> (a -> Assertion) -> Assertion
57−eitherValidationErr (Left e) _ = assertValidtionErr e
58+eitherValidationErr (Left e) _ = assertValidtionErr e
5859eitherValidationErr (Right a) f = f a
5960
6061testParseLocation :: Assertion
6162testParseLocation = do
6263 -- 1. Test parsing of an invalid location constraint xml.
6364 parseResE <- tryValidationErr $ parseLocation "ClearlyInvalidXml"
64− when (isRight parseResE) $
65− assertFailure $ "Parsing should have failed => " ++ show parseResE
65+ when (isRight parseResE)
66+ $ assertFailure
67+ $ "Parsing should have failed => " ++ show parseResE
6668
6769 forM_ cases $ \(xmldata, expectedLocation) -> do
6870 parseLocE <- tryValidationErr $ parseLocation xmldata
6971 either assertValidtionErr (@?= expectedLocation) parseLocE
7072 where
71− cases = [
72− -- 2. Test parsing of a valid location xml.
73− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
74− \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
75− "EU"
76− )
77− ,
78− -- 3. Test parsing of a valid, empty location xml.
79− ("<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
80− "us-east-1"
81− )
73+ cases =
74+ [ -- 2. Test parsing of a valid location xml.
75+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
76+ \<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">EU</LocationConstraint>",
77+ "EU"
78+ ),
79+ -- 3. Test parsing of a valid, empty location xml.
80+ ( "<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"/>",
81+ "us-east-1"
82+ )
8283 ]
8384
84−
8585testParseNewMultipartUpload :: Assertion
8686testParseNewMultipartUpload = do
8787 forM_ cases $ \(xmldata, expectedUploadId) -> do
8888 parsedUploadIdE <- tryValidationErr $ runTestNS $ parseNewMultipartUpload xmldata
8989 eitherValidationErr parsedUploadIdE (@?= expectedUploadId)
9090 where
91− cases = [
92− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
93− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
94− \ <Bucket>example-bucket</Bucket>\
95− \ <Key>example-object</Key>\
96− \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
97− \</InitiateMultipartUploadResult>",
98− "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
99− ),
100− ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
101− \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
102− \ <Bucket>example-bucket</Bucket>\
103− \ <Key>example-object</Key>\
104− \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
105− \</InitiateMultipartUploadResult>",
106− "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
107− )
91+ cases =
92+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
93+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
94+ \ <Bucket>example-bucket</Bucket>\
95+ \ <Key>example-object</Key>\
96+ \ <UploadId>VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA</UploadId>\
97+ \</InitiateMultipartUploadResult>",
98+ "VXBsb2FkIElEIGZvciA2aWWpbmcncyBteS1tb3ZpZS5tMnRzIHVwbG9hZA"
99+ ),
100+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
101+ \<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
102+ \ <Bucket>example-bucket</Bucket>\
103+ \ <Key>example-object</Key>\
104+ \ <UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId>\
105+ \</InitiateMultipartUploadResult>",
106+ "EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-"
107+ )
108108 ]
109109
110110testParseListObjectsResult :: Assertion
111111testParseListObjectsResult = do
112− let
113− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
114− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
115− \<Name>bucket</Name>\
116− \<Prefix/>\
117− \<NextContinuationToken>opaque</NextContinuationToken>\
118− \<KeyCount>1000</KeyCount>\
119− \<MaxKeys>1000</MaxKeys>\
120− \<IsTruncated>true</IsTruncated>\
121− \<Contents>\
122− \<Key>my-image.jpg</Key>\
123− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
124− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
125− \<Size>434234</Size>\
126− \<StorageClass>STANDARD</StorageClass>\
127− \</Contents>\
128− \</ListBucketResult>"
129−
130− expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
131− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
132− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
112+ let xmldata =
113+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
114+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
115+ \<Name>bucket</Name>\
116+ \<Prefix/>\
117+ \<NextContinuationToken>opaque</NextContinuationToken>\
118+ \<KeyCount>1000</KeyCount>\
119+ \<MaxKeys>1000</MaxKeys>\
120+ \<IsTruncated>true</IsTruncated>\
121+ \<Contents>\
122+ \<Key>my-image.jpg</Key>\
123+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
124+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
125+ \<Size>434234</Size>\
126+ \<StorageClass>STANDARD</StorageClass>\
127+ \</Contents>\
128+ \</ListBucketResult>"
129+ expectedListResult = ListObjectsResult True (Just "opaque") [object1] []
130+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
131+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
133132
134133 parsedListObjectsResult <- tryValidationErr $ runTestNS $ parseListObjectsResponse xmldata
135134 eitherValidationErr parsedListObjectsResult (@?= expectedListResult)
136135
137136testParseListObjectsV1Result :: Assertion
138137testParseListObjectsV1Result = do
139− let
140− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
141− \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
142− \<Name>bucket</Name>\
143− \<Prefix/>\
144− \<NextMarker>my-image1.jpg</NextMarker>\
145− \<KeyCount>1000</KeyCount>\
146− \<MaxKeys>1000</MaxKeys>\
147− \<IsTruncated>true</IsTruncated>\
148− \<Contents>\
149− \<Key>my-image.jpg</Key>\
150− \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
151− \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
152− \<Size>434234</Size>\
153− \<StorageClass>STANDARD</StorageClass>\
154− \</Contents>\
155− \</ListBucketResult>"
156−
157− expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
158− object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
159− modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
138+ let xmldata =
139+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
140+ \<ListBucketResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
141+ \<Name>bucket</Name>\
142+ \<Prefix/>\
143+ \<NextMarker>my-image1.jpg</NextMarker>\
144+ \<KeyCount>1000</KeyCount>\
145+ \<MaxKeys>1000</MaxKeys>\
146+ \<IsTruncated>true</IsTruncated>\
147+ \<Contents>\
148+ \<Key>my-image.jpg</Key>\
149+ \<LastModified>2009-10-12T17:50:30.000Z</LastModified>\
150+ \<ETag>&quot;fba9dede5f27731c9771645a39863328&quot;</ETag>\
151+ \<Size>434234</Size>\
152+ \<StorageClass>STANDARD</StorageClass>\
153+ \</Contents>\
154+ \</ListBucketResult>"
155+ expectedListResult = ListObjectsV1Result True (Just "my-image1.jpg") [object1] []
156+ object1 = ObjectInfo "my-image.jpg" modifiedTime1 "\"fba9dede5f27731c9771645a39863328\"" 434234 H.empty H.empty
157+ modifiedTime1 = flip UTCTime 64230 $ fromGregorian 2009 10 12
160158
161159 parsedListObjectsV1Result <- tryValidationErr $ runTestNS $ parseListObjectsV1Response xmldata
162160 eitherValidationErr parsedListObjectsV1Result (@?= expectedListResult)
163161
164162testParseListIncompleteUploads :: Assertion
165163testParseListIncompleteUploads = do
166− let
167− xmldata = "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
168− \<Bucket>example-bucket</Bucket>\
169− \<KeyMarker/>\
170− \<UploadIdMarker/>\
171− \<NextKeyMarker>sample.jpg</NextKeyMarker>\
172− \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
173− \<Delimiter>/</Delimiter>\
174− \<Prefix/>\
175− \<MaxUploads>1000</MaxUploads>\
176− \<IsTruncated>false</IsTruncated>\
177− \<Upload>\
178− \<Key>sample.jpg</Key>\
179− \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
180− \<Initiator>\
181− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
182− \<DisplayName>s3-nickname</DisplayName>\
183− \</Initiator>\
184− \<Owner>\
185− \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
186− \<DisplayName>s3-nickname</DisplayName>\
187− \</Owner>\
188− \<StorageClass>STANDARD</StorageClass>\
189− \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
190− \</Upload>\
191− \<CommonPrefixes>\
192− \<Prefix>photos/</Prefix>\
193− \</CommonPrefixes>\
194− \<CommonPrefixes>\
195− \<Prefix>videos/</Prefix>\
196− \</CommonPrefixes>\
197− \</ListMultipartUploadsResult>"
198− expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
199− uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
200− initTime = UTCTime (fromGregorian 2010 11 26) 69857
201− prefixes = ["photos/", "videos/"]
164+ let xmldata =
165+ "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
166+ \<Bucket>example-bucket</Bucket>\
167+ \<KeyMarker/>\
168+ \<UploadIdMarker/>\
169+ \<NextKeyMarker>sample.jpg</NextKeyMarker>\
170+ \<NextUploadIdMarker>Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</NextUploadIdMarker>\
171+ \<Delimiter>/</Delimiter>\
172+ \<Prefix/>\
173+ \<MaxUploads>1000</MaxUploads>\
174+ \<IsTruncated>false</IsTruncated>\
175+ \<Upload>\
176+ \<Key>sample.jpg</Key>\
177+ \<UploadId>Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--</UploadId>\
178+ \<Initiator>\
179+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
180+ \<DisplayName>s3-nickname</DisplayName>\
181+ \</Initiator>\
182+ \<Owner>\
183+ \<ID>314133b66967d86f031c7249d1d9a80249109428335cd0ef1cdc487b4566cb1b</ID>\
184+ \<DisplayName>s3-nickname</DisplayName>\
185+ \</Owner>\
186+ \<StorageClass>STANDARD</StorageClass>\
187+ \<Initiated>2010-11-26T19:24:17.000Z</Initiated>\
188+ \</Upload>\
189+ \<CommonPrefixes>\
190+ \<Prefix>photos/</Prefix>\
191+ \</CommonPrefixes>\
192+ \<CommonPrefixes>\
193+ \<Prefix>videos/</Prefix>\
194+ \</CommonPrefixes>\
195+ \</ListMultipartUploadsResult>"
196+ expectedListResult = ListUploadsResult False (Just "sample.jpg") (Just "Xgw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1W99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--") uploads prefixes
197+ uploads = [("sample.jpg", "Agw4MJT6ZPAVxpY0SAuGN7q4uWJJM22ZYg1N99trdp4tpO88.PT6.MhO0w2E17eutfAvQfQWoajgE_W2gpcxQw--", initTime)]
198+ initTime = UTCTime (fromGregorian 2010 11 26) 69857
199+ prefixes = ["photos/", "videos/"]
202200
203201 parsedListUploadsResult <- tryValidationErr $ runTestNS $ parseListUploadsResponse xmldata
204202 eitherValidationErr parsedListUploadsResult (@?= expectedListResult)
205203
206−
207204testParseCompleteMultipartUploadResponse :: Assertion
208205testParseCompleteMultipartUploadResponse = do
209− let
210− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
211−\<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
212− \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
213− \<Bucket>Example-Bucket</Bucket>\
214− \<Key>Example-Object</Key>\
215− \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
216−\</CompleteMultipartUploadResult>"
217− expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
206+ let xmldata =
207+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
208+ \<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
209+ \<Location>http://Example-Bucket.s3.amazonaws.com/Example-Object</Location>\
210+ \<Bucket>Example-Bucket</Bucket>\
211+ \<Key>Example-Object</Key>\
212+ \<ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag>\
213+ \</CompleteMultipartUploadResult>"
214+ expectedETag = "\"3858f62230ac3c915f300c664312c11f-9\""
218215
219216 parsedETagE <- runExceptT $ runTestNS $ parseCompleteMultipartUploadResponse xmldata
220217 eitherValidationErr parsedETagE (@?= expectedETag)
221218
222219testParseListPartsResponse :: Assertion
223220testParseListPartsResponse = do
224− let
225− xmldata = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
226−\<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
227− \<Bucket>example-bucket</Bucket>\
228− \<Key>example-object</Key>\
229− \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
230− \<Initiator>\
231− \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
232− \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
233− \</Initiator>\
234− \<Owner>\
235− \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
236− \<DisplayName>someName</DisplayName>\
237− \</Owner>\
238− \<StorageClass>STANDARD</StorageClass>\
239− \<PartNumberMarker>1</PartNumberMarker>\
240− \<NextPartNumberMarker>3</NextPartNumberMarker>\
241− \<MaxParts>2</MaxParts>\
242− \<IsTruncated>true</IsTruncated>\
243− \<Part>\
244− \<PartNumber>2</PartNumber>\
245− \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
246− \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
247− \<Size>10485760</Size>\
248− \</Part>\
249− \<Part>\
250− \<PartNumber>3</PartNumber>\
251− \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
252− \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
253− \<Size>10485760</Size>\
254− \</Part>\
255−\</ListPartsResult>"
256−
257− expectedListResult = ListPartsResult True (Just 3) [part1, part2]
258− part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
259− modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
260− part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
261− modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
221+ let xmldata =
222+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
223+ \<ListPartsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
224+ \<Bucket>example-bucket</Bucket>\
225+ \<Key>example-object</Key>\
226+ \<UploadId>XXBsb2FkIElEIGZvciBlbHZpbmcncyVcdS1tb3ZpZS5tMnRzEEEwbG9hZA</UploadId>\
227+ \<Initiator>\
228+ \<ID>arn:aws:iam::111122223333:user/some-user-11116a31-17b5-4fb7-9df5-b288870f11xx</ID>\
229+ \<DisplayName>umat-user-11116a31-17b5-4fb7-9df5-b288870f11xx</DisplayName>\
230+ \</Initiator>\
231+ \<Owner>\
232+ \<ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID>\
233+ \<DisplayName>someName</DisplayName>\
234+ \</Owner>\
235+ \<StorageClass>STANDARD</StorageClass>\
236+ \<PartNumberMarker>1</PartNumberMarker>\
237+ \<NextPartNumberMarker>3</NextPartNumberMarker>\
238+ \<MaxParts>2</MaxParts>\
239+ \<IsTruncated>true</IsTruncated>\
240+ \<Part>\
241+ \<PartNumber>2</PartNumber>\
242+ \<LastModified>2010-11-10T20:48:34.000Z</LastModified>\
243+ \<ETag>\"7778aef83f66abc1fa1e8477f296d394\"</ETag>\
244+ \<Size>10485760</Size>\
245+ \</Part>\
246+ \<Part>\
247+ \<PartNumber>3</PartNumber>\
248+ \<LastModified>2010-11-10T20:48:33.000Z</LastModified>\
249+ \<ETag>\"aaaa18db4cc2f85cedef654fccc4a4x8\"</ETag>\
250+ \<Size>10485760</Size>\
251+ \</Part>\
252+ \</ListPartsResult>"
253+ expectedListResult = ListPartsResult True (Just 3) [part1, part2]
254+ part1 = ObjectPartInfo 2 "\"7778aef83f66abc1fa1e8477f296d394\"" 10485760 modifiedTime1
255+ modifiedTime1 = flip UTCTime 74914 $ fromGregorian 2010 11 10
256+ part2 = ObjectPartInfo 3 "\"aaaa18db4cc2f85cedef654fccc4a4x8\"" 10485760 modifiedTime2
257+ modifiedTime2 = flip UTCTime 74913 $ fromGregorian 2010 11 10
262258
263259 parsedListPartsResult <- runExceptT $ runTestNS $ parseListPartsResponse xmldata
264260 eitherValidationErr parsedListPartsResult (@?= expectedListResult)
265261
266262testParseCopyObjectResponse :: Assertion
267263testParseCopyObjectResponse = do
268− let
269− cases = [ ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
270−\<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
271− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
272− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
273−\</CopyObjectResult>",
274− ("\"9b2cf535f27731c974343645a3985328\"",
275− UTCTime (fromGregorian 2009 10 28) 81120))
276− , ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
277−\<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
278− \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
279− \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
280−\</CopyPartResult>",
281− ("\"9b2cf535f27731c974343645a3985328\"",
282− UTCTime (fromGregorian 2009 10 28) 81120))]
264+ let cases =
265+ [ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
266+ \<CopyObjectResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
267+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
268+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
269+ \</CopyObjectResult>",
270+ ( "\"9b2cf535f27731c974343645a3985328\"",
271+ UTCTime (fromGregorian 2009 10 28) 81120
272+ )
273+ ),
274+ ( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
275+ \<CopyPartResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
276+ \<LastModified>2009-10-28T22:32:00.000Z</LastModified>\
277+ \<ETag>\"9b2cf535f27731c974343645a3985328\"</ETag>\
278+ \</CopyPartResult>",
279+ ( "\"9b2cf535f27731c974343645a3985328\"",
280+ UTCTime (fromGregorian 2009 10 28) 81120
281+ )
282+ )
283+ ]
283284
284285 forM_ cases $ \(xmldata, (etag, modTime)) -> do
285286 parseResult <- runExceptT $ runTestNS $ parseCopyObjectResponse xmldata
@@ -287,73 +288,88 @@ testParseCopyObjectResponse = do
287288
288289testParseNotification :: Assertion
289290testParseNotification = do
290− let
291− cases = [ ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
292−\ <TopicConfiguration>\
293−\ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
294−\ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
295−\ <Event>s3:ReducedRedundancyLostObject</Event>\
296−\ <Event>s3:ObjectCreated:*</Event>\
297−\ </TopicConfiguration>\
298−\</NotificationConfiguration>",
299− Notification []
300− [ NotificationConfig
291+ let cases =
292+ [ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
293+ \ <TopicConfiguration>\
294+ \ <Id>YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4</Id>\
295+ \ <Topic>arn:aws:sns:us-east-1:account-id:s3notificationtopic2</Topic>\
296+ \ <Event>s3:ReducedRedundancyLostObject</Event>\
297+ \ <Event>s3:ObjectCreated:*</Event>\
298+ \ </TopicConfiguration>\
299+ \</NotificationConfiguration>",
300+ Notification
301+ []
302+ [ NotificationConfig
301303 "YjVkM2Y0YmUtNGI3NC00ZjQyLWEwNGItNDIyYWUxY2I0N2M4"
302304 "arn:aws:sns:us-east-1:account-id:s3notificationtopic2"
303− [ReducedRedundancyLostObject, ObjectCreated] defaultFilter
304− ]
305− [])
306− , ("<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
307−\ <CloudFunctionConfiguration>\
308−\ <Id>ObjectCreatedEvents</Id>\
309−\ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
310−\ <Event>s3:ObjectCreated:*</Event>\
311−\ </CloudFunctionConfiguration>\
312−\ <QueueConfiguration>\
313−\ <Id>1</Id>\
314−\ <Filter>\
315−\ <S3Key>\
316−\ <FilterRule>\
317−\ <Name>prefix</Name>\
318−\ <Value>images/</Value>\
319−\ </FilterRule>\
320−\ <FilterRule>\
321−\ <Name>suffix</Name>\
322−\ <Value>.jpg</Value>\
323−\ </FilterRule>\
324−\ </S3Key>\
325−\ </Filter>\
326−\ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
327−\ <Event>s3:ObjectCreated:Put</Event>\
328−\ </QueueConfiguration>\
329−\ <TopicConfiguration>\
330−\ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
331−\ <Event>s3:ReducedRedundancyLostObject</Event>\
332−\ </TopicConfiguration>\
333−\ <QueueConfiguration>\
334−\ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
335−\ <Event>s3:ObjectCreated:*</Event>\
336−\ </QueueConfiguration>)\
337−\</NotificationConfiguration>",
338− Notification [ NotificationConfig
339− "1" "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
340− [ObjectCreatedPut]
341− (Filter $ FilterKey $ FilterRules
342− [FilterRule "prefix" "images/",
343− FilterRule "suffix" ".jpg"])
344− , NotificationConfig
345− "" "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
346− [ObjectCreated] defaultFilter
347− ]
348− [ NotificationConfig
349− "" "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
350− [ReducedRedundancyLostObject] defaultFilter
351− ]
352− [ NotificationConfig
353− "ObjectCreatedEvents" "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
354− [ObjectCreated] defaultFilter
355− ])
356− ]
305+ [ReducedRedundancyLostObject, ObjectCreated]
306+ defaultFilter
307+ ]
308+ []
309+ ),
310+ ( "<NotificationConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
311+ \ <CloudFunctionConfiguration>\
312+ \ <Id>ObjectCreatedEvents</Id>\
313+ \ <CloudFunction>arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail</CloudFunction>\
314+ \ <Event>s3:ObjectCreated:*</Event>\
315+ \ </CloudFunctionConfiguration>\
316+ \ <QueueConfiguration>\
317+ \ <Id>1</Id>\
318+ \ <Filter>\
319+ \ <S3Key>\
320+ \ <FilterRule>\
321+ \ <Name>prefix</Name>\
322+ \ <Value>images/</Value>\
323+ \ </FilterRule>\
324+ \ <FilterRule>\
325+ \ <Name>suffix</Name>\
326+ \ <Value>.jpg</Value>\
327+ \ </FilterRule>\
328+ \ </S3Key>\
329+ \ </Filter>\
330+ \ <Queue>arn:aws:sqs:us-west-2:444455556666:s3notificationqueue</Queue>\
331+ \ <Event>s3:ObjectCreated:Put</Event>\
332+ \ </QueueConfiguration>\
333+ \ <TopicConfiguration>\
334+ \ <Topic>arn:aws:sns:us-east-1:356671443308:s3notificationtopic2</Topic>\
335+ \ <Event>s3:ReducedRedundancyLostObject</Event>\
336+ \ </TopicConfiguration>\
337+ \ <QueueConfiguration>\
338+ \ <Queue>arn:aws:sqs:us-east-1:356671443308:s3notificationqueue</Queue>\
339+ \ <Event>s3:ObjectCreated:*</Event>\
340+ \ </QueueConfiguration>)\
341+ \</NotificationConfiguration>",
342+ Notification
343+ [ NotificationConfig
344+ "1"
345+ "arn:aws:sqs:us-west-2:444455556666:s3notificationqueue"
346+ [ObjectCreatedPut]
347+ ( Filter $ FilterKey $
348+ FilterRules
349+ [ FilterRule "prefix" "images/",
350+ FilterRule "suffix" ".jpg"
351+ ]
352+ ),
353+ NotificationConfig
354+ ""
355+ "arn:aws:sqs:us-east-1:356671443308:s3notificationqueue"
356+ [ObjectCreated]
357+ defaultFilter
358+ ]
359+ [ NotificationConfig
360+ ""
361+ "arn:aws:sns:us-east-1:356671443308:s3notificationtopic2"
362+ [ReducedRedundancyLostObject]
363+ defaultFilter
364+ ]
365+ [ NotificationConfig
366+ "ObjectCreatedEvents"
367+ "arn:aws:lambda:us-west-2:35667example:function:CreateThumbnail"
368+ [ObjectCreated]
369+ defaultFilter
370+ ]
371+ )
372+ ]
357373
358374 forM_ cases $ \(xmldata, val) -> do
359375 result <- runExceptT $ runTestNS $ parseNotification xmldata
@@ -362,20 +378,25 @@ testParseNotification = do
362378-- | Tests parsing of both progress and stats
363379testParseSelectProgress :: Assertion
364380testParseSelectProgress = do
365− let cases = [ ([r|<?xml version="1.0" encoding="UTF-8"?>
381+ let cases =
382+ [ ( [r|<?xml version="1.0" encoding="UTF-8"?>
366383<Progress>
367384 <BytesScanned>512</BytesScanned>
368385 <BytesProcessed>1024</BytesProcessed>
369386 <BytesReturned>1024</BytesReturned>
370−</Progress>|] , Progress 512 1024 1024)
371− , ([r|<?xml version="1.0" encoding="UTF-8"?>
387+</Progress>|],
388+ Progress 512 1024 1024
389+ ),
390+ ( [r|<?xml version="1.0" encoding="UTF-8"?>
372391<Stats>
373392 <BytesScanned>512</BytesScanned>
374393 <BytesProcessed>1024</BytesProcessed>
375394 <BytesReturned>1024</BytesReturned>
376−</Stats>|], Progress 512 1024 1024)
377− ]
378−
379− forM_ cases $ \(xmldata, progress) -> do
380− result <- runExceptT $ parseSelectProgress xmldata
381− eitherValidationErr result (@?= progress)
395+</Stats>|],
396+ Progress 512 1024 1024
397+ )
398+ ]
399+
400+ forM_ cases $ \(xmldata, progress) -> do
401+ result <- runExceptT $ parseSelectProgress xmldata
402+ eitherValidationErr result (@?= progress)
test/Spec.hs+78−79
@@ -14,21 +14,18 @@
1414-- limitations under the License.
1515--
1616
17−import Test.Tasty
18−import Test.Tasty.QuickCheck as QC
19−
20−import qualified Data.ByteString as B
21−import qualified Data.List as L
22−
23−import Lib.Prelude
24−
25−import Network.Minio.API.Test
26−import Network.Minio.CopyObject
27−import Network.Minio.Data
28−import Network.Minio.PutObject
29−import Network.Minio.Utils.Test
30−import Network.Minio.XmlGenerator.Test
31−import Network.Minio.XmlParser.Test
17+import qualified Data.ByteString as B
18+import qualified Data.List as L
19+import Lib.Prelude
20+import Network.Minio.API.Test
21+import Network.Minio.CopyObject
22+import Network.Minio.Data
23+import Network.Minio.PutObject
24+import Network.Minio.Utils.Test
25+import Network.Minio.XmlGenerator.Test
26+import Network.Minio.XmlParser.Test
27+import Test.Tasty
28+import Test.Tasty.QuickCheck as QC
3229
3330main :: IO ()
3431main = defaultMain tests
@@ -51,82 +48,84 @@ properties = testGroup "Properties" [qcProps] -- [scProps]
5148-- ]
5249
5350qcProps :: TestTree
54−qcProps = testGroup "(checked by QuickCheck)"
55− [ QC.testProperty "selectPartSizes:" $
56− \n -> let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
57−
51+qcProps =
52+ testGroup
53+ "(checked by QuickCheck)"
54+ [ QC.testProperty "selectPartSizes:" $
55+ \n ->
56+ let (pns, offs, sizes) = L.unzip3 (selectPartSizes n)
5857 -- check that pns increments from 1.
59− isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1..]
60−
61− consPairs [] = []
62− consPairs [_] = []
63− consPairs (a:(b:c)) = (a, b):(consPairs (b:c))
64−
58+ isPNumsAscendingFrom1 = all (\(a, b) -> a == b) $ zip pns [1 ..]
59+ consPairs [] = []
60+ consPairs [_] = []
61+ consPairs (a : (b : c)) = (a, b) : (consPairs (b : c))
6562 -- check `offs` is monotonically increasing.
6663 isOffsetsAsc = all (\(a, b) -> a < b) $ consPairs offs
67−
6864 -- check sizes sums to n.
6965 isSumSizeOk = sum sizes == n
70−
7166 -- check sizes are constant except last
7267 isSizesConstantExceptLast =
7368 all (\(a, b) -> a == b) (consPairs $ L.init sizes)
74−
7569 -- check each part except last is at least minPartSize;
7670 -- last part may be 0 only if it is the only part.
7771 nparts = length sizes
7872 isMinPartSizeOk =
79− if | nparts > 1 -> -- last part can be smaller but > 0
80− all (>= minPartSize) (take (nparts - 1) sizes) &&
81− all (\s -> s > 0) (drop (nparts - 1) sizes)
82− | nparts == 1 -> -- size may be 0 here.
83− maybe True (\x -> x >= 0 && x <= minPartSize) $
84− headMay sizes
85− | otherwise -> False
86−
87− in n < 0 ||
88− (isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk &&
89− isSizesConstantExceptLast && isMinPartSizeOk)
90−
91− , QC.testProperty "selectCopyRanges:" $
92− \(start, end) ->
93− let (_, pairs) = L.unzip (selectCopyRanges (start, end))
94−
95− -- is last part's snd offset end?
96− isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
97− -- is first part's fst offset start
98− isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs
99−
100− -- each pair is >=64MiB except last, and all those parts
101− -- have same size.
102− initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs
103− isPartSizesOk = all (>= minPartSize) initSizes &&
104− maybe True (\k -> all (== k) initSizes)
105− (headMay initSizes)
106−
107− -- returned offsets are contiguous.
108− fsts = drop 1 $ map fst pairs
109− snds = take (length pairs - 1) $ map snd pairs
110− isContParts = length fsts == length snds &&
111− and (map (\(a, b) -> a == b + 1) $ zip fsts snds)
112−
113− in start < 0 || start > end ||
114− (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts)
115−
116− , QC.testProperty "mkSSECKey:" $
117− \w8s -> let bs = B.pack w8s
118− r = mkSSECKey bs
119− in case r of
120− Just _ -> B.length bs == 32
73+ if
74+ | nparts > 1 -> -- last part can be smaller but > 0
75+ all (>= minPartSize) (take (nparts - 1) sizes)
76+ && all (\s -> s > 0) (drop (nparts - 1) sizes)
77+ | nparts == 1 -> -- size may be 0 here.
78+ maybe True (\x -> x >= 0 && x <= minPartSize) $
79+ headMay sizes
80+ | otherwise -> False
81+ in n < 0
82+ || ( isPNumsAscendingFrom1 && isOffsetsAsc && isSumSizeOk
83+ && isSizesConstantExceptLast
84+ && isMinPartSizeOk
85+ ),
86+ QC.testProperty "selectCopyRanges:" $
87+ \(start, end) ->
88+ let (_, pairs) = L.unzip (selectCopyRanges (start, end))
89+ -- is last part's snd offset end?
90+ isLastPartOk = maybe False ((end ==) . snd) $ lastMay pairs
91+ -- is first part's fst offset start
92+ isFirstPartOk = maybe False ((start ==) . fst) $ headMay pairs
93+ -- each pair is >=64MiB except last, and all those parts
94+ -- have same size.
95+ initSizes = maybe [] (map (\(a, b) -> b - a + 1)) $ initMay pairs
96+ isPartSizesOk =
97+ all (>= minPartSize) initSizes
98+ && maybe
99+ True
100+ (\k -> all (== k) initSizes)
101+ (headMay initSizes)
102+ -- returned offsets are contiguous.
103+ fsts = drop 1 $ map fst pairs
104+ snds = take (length pairs - 1) $ map snd pairs
105+ isContParts =
106+ length fsts == length snds
107+ && and (map (\(a, b) -> a == b + 1) $ zip fsts snds)
108+ in start < 0 || start > end
109+ || (isLastPartOk && isFirstPartOk && isPartSizesOk && isContParts),
110+ QC.testProperty "mkSSECKey:" $
111+ \w8s ->
112+ let bs = B.pack w8s
113+ r = mkSSECKey bs
114+ in case r of
115+ Just _ -> B.length bs == 32
121116 Nothing -> B.length bs /= 32
122− ]
117+ ]
123118
124119unitTests :: TestTree
125−unitTests = testGroup "Unit tests" [ xmlGeneratorTests, xmlParserTests
126− , bucketNameValidityTests
127− , objectNameValidityTests
128− , parseServerInfoJSONTest
129− , parseHealStatusTest
130− , parseHealStartRespTest
131− , limitedMapConcurrentlyTests
132− ]
120+unitTests =
121+ testGroup
122+ "Unit tests"
123+ [ xmlGeneratorTests,
124+ xmlParserTests,
125+ bucketNameValidityTests,
126+ objectNameValidityTests,
127+ parseServerInfoJSONTest,
128+ parseHealStatusTest,
129+ parseHealStartRespTest,
130+ limitedMapConcurrentlyTests
131+ ]