git.iko.soy/typeable/minio-hs
on master help sign in
Add support for S3Select API (#108)
commit72bf08129cc91b898b8a44034418077d65e57e17 authorAditya Manthramurthy <donatello@users.noreply.github.com> dateFri 8 Mar 2019, 15:54 (8 years ago) parentab7d04b
12 files changed+818−16
docs/API.md+54−1 examples/SelectObject.hs+50 minio-hs.cabal+17−2 src/Network/Minio.hs+4 src/Network/Minio/Data.hs+235−2 src/Network/Minio/Errors.hs+1 src/Network/Minio/SelectAPI.hs+302 src/Network/Minio/XmlGenerator.hs+71−8 src/Network/Minio/XmlParser.hs+12 stack.yaml+1−1 test/Network/Minio/XmlGenerator/Test.hs+46 test/Network/Minio/XmlParser/Test.hs+25−2
docs/API.md+54−1
@@ -28,7 +28,7 @@ awsCI { connectAccesskey = "your-access-key"
2828|[`listObjects`](#listObjects)|[`fPutObject`](#fPutObject)||
2929|[`listObjectsV1`](#listObjectsV1)|[`copyObject`](#copyObject)||
3030|[`listIncompleteUploads`](#listIncompleteUploads)|[`removeObject`](#removeObject)||
31−|[`bucketExists`](#bucketExists)|||
31+|[`bucketExists`](#bucketExists)|[`selectObjectContent`](#selectObjectContent)||
3232
3333## 1. Connecting and running operations on the storage service
3434
@@ -743,6 +743,59 @@ main = do
743743 Right _ -> putStrLn "Removed incomplete upload successfully"
744744```
745745
746+<a name="selectObjectContent"></a>
747+### selectObjectContent :: Bucket -> Object -> SelectRequest -> Minio (ConduitT () EventMessage Minio ())
748+Removes an ongoing multipart upload of an object from the service
749+
750+__Parameters__
751+
752+In the expression `selectObjectContent bucketName objectName selReq`
753+the parameters are:
754+
755+|Param |Type |Description |
756+|:---|:---| :---|
757+| `bucketName` | _Bucket_ (alias for `Text`) | Name of the bucket |
758+| `objectName` | _Object_ (alias for `Text`) | Name of the object |
759+| `selReq` | _SelectRequest_ | Select request parameters |
760+
761+__SelectRequest record__
762+
763+This record is created using `selectRequest`. Please refer to the Haddocks for further information.
764+
765+__Return Value__
766+
767+The return value can be used to read individual `EventMessage`s in the response. Please refer to the Haddocks for further information.
768+
769+|Return type | Description |
770+|:---|:---|
771+| _Minio (C.conduitT () EventMessage Minio ())_ | A Conduit source of `EventMessage` values. |
772+
773+__Example__
774+
775+```haskell
776+{-# Language OverloadedStrings #-}
777+import Network.Minio
778+
779+import qualified Conduit as C
780+
781+main :: IO ()
782+main = do
783+ let
784+ bucket = "mybucket"
785+ object = "myobject"
786+
787+ res <- runMinio minioPlayCI $ do
788+ let sr = selectRequest "Select * from s3object"
789+ defaultCsvInput defaultCsvOutput
790+ res <- selectObjectContent bucket object sr
791+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
792+
793+ case res of
794+ Left _ -> putStrLn "Failed!"
795+ Right _ -> putStrLn "Success!"
796+```
797+
798+
746799<a name="BucketExists"></a>
747800### bucketExists :: Bucket -> Minio Bool
748801Checks if a bucket exists.
examples/SelectObject.hsnew file+50
@@ -0,0 +1,50 @@
1+#!/usr/bin/env stack
2+-- stack --resolver lts-13.1 runghc --package minio-hs
3+
4+--
5+-- Minio Haskell SDK, (C) 2019 Minio, Inc.
6+--
7+-- Licensed under the Apache License, Version 2.0 (the "License");
8+-- you may not use this file except in compliance with the License.
9+-- You may obtain a copy of the License at
10+--
11+-- http://www.apache.org/licenses/LICENSE-2.0
12+--
13+-- Unless required by applicable law or agreed to in writing, software
14+-- distributed under the License is distributed on an "AS IS" BASIS,
15+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+-- See the License for the specific language governing permissions and
17+-- limitations under the License.
18+--
19+
20+{-# LANGUAGE OverloadedStrings #-}
21+import Network.Minio
22+
23+import qualified Conduit as C
24+import Control.Monad (when)
25+import qualified Data.ByteString.Lazy as LB
26+
27+import Prelude
28+
29+main :: IO ()
30+main = do
31+ let bucket = "selectbucket"
32+ object = "1.csv"
33+ content = "Name,Place,Temperature\n"
34+ <> "James,San Jose,76\n"
35+ <> "Alicia,San Leandro,88\n"
36+ <> "Mark,San Carlos,90\n"
37+
38+ res <- runMinio minioPlayCI $ do
39+
40+ exists <- bucketExists bucket
41+ when (not exists) $
42+ makeBucket bucket Nothing
43+
44+ C.liftIO $ putStrLn "Uploading csv object"
45+ putObject bucket object (C.sourceLazy content) Nothing defaultPutObjectOptions
46+
47+ let sr = selectRequest "Select * from s3object" defaultCsvInput defaultCsvOutput
48+ res <- selectObjectContent bucket object sr
49+ C.runConduit $ res C..| getPayloadBytes C..| C.stdoutC
50+ print res
minio-hs.cabal+17−2
@@ -41,6 +41,7 @@ library
4141 , Network.Minio.ListOps
4242 , Network.Minio.PresignedOperations
4343 , Network.Minio.PutObject
44+ , Network.Minio.SelectAPI
4445 , Network.Minio.Sign.V4
4546 , Network.Minio.Utils
4647 , Network.Minio.XmlGenerator
@@ -50,6 +51,7 @@ library
5051 , protolude >= 0.2 && < 0.3
5152 , aeson >= 1.2
5253 , base64-bytestring >= 1.0
54+ , binary >= 0.8.5.0
5355 , bytestring >= 0.10
5456 , case-insensitive >= 1.2
5557 , conduit >= 1.3
@@ -57,6 +59,7 @@ library
5759 , containers >= 0.5
5860 , cryptonite >= 0.25
5961 , cryptonite-conduit >= 0.2
62+ , digest >= 0.0.1
6063 , directory
6164 , filepath >= 1.4
6265 , http-client >= 0.5
@@ -64,12 +67,14 @@ library
6467 , http-types >= 0.12
6568 , ini
6669 , memory >= 0.14
70+ , raw-strings-qq >= 1
6771 , resourcet >= 1.2
6872 , text >= 1.2
6973 , time >= 1.8
7074 , transformers >= 0.5
7175 , unliftio >= 0.2
7276 , unliftio-core >= 0.1
77+ , unordered-containers >= 0.2
7378 , xml-conduit >= 1.8
7479 default-language: Haskell2010
7580 default-extensions: BangPatterns
@@ -120,6 +125,7 @@ test-suite minio-hs-live-server-test
120125 , Network.Minio.PresignedOperations
121126 , Network.Minio.PutObject
122127 , Network.Minio.S3API
128+ , Network.Minio.SelectAPI
123129 , Network.Minio.Sign.V4
124130 , Network.Minio.TestHelpers
125131 , Network.Minio.Utils
@@ -131,11 +137,12 @@ test-suite minio-hs-live-server-test
131137 , Network.Minio.XmlParser.Test
132138 , Network.Minio.JsonParser
133139 , Network.Minio.JsonParser.Test
134− build-depends: base
140+ build-depends: base >= 4.7 && < 5
135141 , minio-hs
136142 , protolude >= 0.1.6
137143 , aeson
138144 , base64-bytestring
145+ , binary
139146 , bytestring
140147 , case-insensitive
141148 , conduit
@@ -143,6 +150,7 @@ test-suite minio-hs-live-server-test
143150 , containers
144151 , cryptonite
145152 , cryptonite-conduit
153+ , digest
146154 , directory
147155 , filepath
148156 , http-client
@@ -151,6 +159,7 @@ test-suite minio-hs-live-server-test
151159 , ini
152160 , memory
153161 , QuickCheck
162+ , raw-strings-qq >= 1
154163 , resourcet
155164 , tasty
156165 , tasty-hunit
@@ -162,6 +171,7 @@ test-suite minio-hs-live-server-test
162171 , transformers
163172 , unliftio
164173 , unliftio-core
174+ , unordered-containers
165175 , xml-conduit
166176 if !flag(live-test)
167177 buildable: False
@@ -170,11 +180,12 @@ test-suite minio-hs-test
170180 type: exitcode-stdio-1.0
171181 hs-source-dirs: test, src
172182 main-is: Spec.hs
173− build-depends: base
183+ build-depends: base >= 4.7 && < 5
174184 , minio-hs
175185 , protolude >= 0.1.6
176186 , aeson
177187 , base64-bytestring
188+ , binary
178189 , bytestring
179190 , case-insensitive
180191 , conduit
@@ -183,6 +194,7 @@ test-suite minio-hs-test
183194 , cryptonite
184195 , cryptonite-conduit
185196 , filepath
197+ , digest
186198 , directory
187199 , http-client
188200 , http-conduit
@@ -190,6 +202,7 @@ test-suite minio-hs-test
190202 , ini
191203 , memory
192204 , QuickCheck
205+ , raw-strings-qq >= 1
193206 , resourcet
194207 , tasty
195208 , tasty-hunit
@@ -201,6 +214,7 @@ test-suite minio-hs-test
201214 , transformers
202215 , unliftio
203216 , unliftio-core
217+ , unordered-containers
204218 , xml-conduit
205219 ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
206220 default-language: Haskell2010
@@ -230,6 +244,7 @@ test-suite minio-hs-test
230244 , Network.Minio.PresignedOperations
231245 , Network.Minio.PutObject
232246 , Network.Minio.S3API
247+ , Network.Minio.SelectAPI
233248 , Network.Minio.Sign.V4
234249 , Network.Minio.TestHelpers
235250 , Network.Minio.Utils
src/Network/Minio.hs+4
@@ -158,6 +158,9 @@ module Network.Minio
158158 , removeObject
159159 , removeIncompleteUpload
160160
161+ -- ** Select Object Content with SQL
162+ , module Network.Minio.SelectAPI
163+
161164 -- * Presigned Operations
162165 -------------------------
163166 , UrlExpiry
@@ -207,6 +210,7 @@ import Network.Minio.Errors
207210import Network.Minio.ListOps
208211import Network.Minio.PutObject
209212import Network.Minio.S3API
213+import Network.Minio.SelectAPI
210214import Network.Minio.Utils
211215
212216-- | Lists buckets.
src/Network/Minio/Data.hs+235−2
@@ -14,6 +14,7 @@
1414-- limitations under the License.
1515--
1616
17+{-# LANGUAGE CPP #-}
1718{-# LANGUAGE GeneralizedNewtypeDeriving #-}
1819{-# LANGUAGE TypeFamilies #-}
1920module Network.Minio.Data where
@@ -25,6 +26,7 @@ import Control.Monad.IO.Unlift (MonadUnliftIO, UnliftIO (..),
2526import Control.Monad.Trans.Resource
2627import qualified Data.ByteString as B
2728import Data.CaseInsensitive (mk)
29+import qualified Data.HashMap.Strict as H
2830import qualified Data.Ini as Ini
2931import qualified Data.Map as Map
3032import Data.String (IsString (..))
@@ -511,6 +513,237 @@ data Notification = Notification
511513defaultNotification :: Notification
512514defaultNotification = Notification [] [] []
513515
516+
517+--------------------------------------------------------------------------
518+-- Select API Related Types
519+--------------------------------------------------------------------------
520+
521+-- | SelectRequest represents the Select API call. Use the
522+-- `selectRequest` function to create a value of this type.
523+data SelectRequest = SelectRequest
524+ { srExpression :: Text
525+ , srExpressionType :: ExpressionType
526+ , srInputSerialization :: InputSerialization
527+ , srOutputSerialization :: OutputSerialization
528+ , srRequestProgressEnabled :: Maybe Bool
529+ } deriving (Eq, Show)
530+
531+data ExpressionType = SQL
532+ deriving (Eq, Show)
533+
534+-- | InputSerialization represents format information of the input
535+-- object being queried. Use one of the smart constructors such as
536+-- `defaultCsvInput` as a starting value, and add compression info
537+-- using `setInputCompressionType`
538+data InputSerialization = InputSerialization
539+ { isCompressionType :: Maybe CompressionType
540+ , isFormatInfo :: InputFormatInfo
541+ } deriving (Eq, Show)
542+
543+data CompressionType = CompressionTypeNone
544+ | CompressionTypeGzip
545+ | CompressionTypeBzip2
546+ deriving (Eq, Show)
547+
548+data InputFormatInfo = InputFormatCSV CSVInputProp
549+ | InputFormatJSON JSONInputProp
550+ | InputFormatParquet
551+ deriving (Eq, Show)
552+
553+-- | defaultCsvInput returns InputSerialization with default CSV
554+-- format, and without any compression setting.
555+defaultCsvInput :: InputSerialization
556+defaultCsvInput = InputSerialization Nothing (InputFormatCSV defaultCSVProp)
557+
558+-- | linesJsonInput returns InputSerialization with JSON line based
559+-- format with no compression setting.
560+linesJsonInput :: InputSerialization
561+linesJsonInput = InputSerialization Nothing
562+ (InputFormatJSON $ JSONInputProp JSONTypeLines)
563+
564+-- | documentJsonInput returns InputSerialization with JSON document
565+-- based format with no compression setting.
566+documentJsonInput :: InputSerialization
567+documentJsonInput = InputSerialization Nothing
568+ (InputFormatJSON $ JSONInputProp JSONTypeDocument)
569+
570+-- | defaultParquetInput returns InputSerialization with Parquet
571+-- format, and no compression setting.
572+defaultParquetInput :: InputSerialization
573+defaultParquetInput = InputSerialization Nothing InputFormatParquet
574+
575+-- | setInputCompressionType sets the compression type for the input
576+-- of the SelectRequest
577+setInputCompressionType :: CompressionType -> SelectRequest
578+ -> SelectRequest
579+setInputCompressionType c i =
580+ let is = srInputSerialization i
581+ is' = is { isCompressionType = Just c }
582+ in i { srInputSerialization = is' }
583+
584+-- | defaultCsvOutput returns OutputSerialization with default CSV
585+-- format.
586+defaultCsvOutput :: OutputSerialization
587+defaultCsvOutput = OutputSerializationCSV defaultCSVProp
588+
589+-- | defaultJsonInput returns OutputSerialization with default JSON
590+-- format.
591+defaultJsonOutput :: OutputSerialization
592+defaultJsonOutput = OutputSerializationJSON (JSONOutputProp Nothing)
593+
594+-- | selectRequest is used to build a `SelectRequest`
595+-- value. @selectRequest query inputSer outputSer@ represents a
596+-- SelectRequest with the SQL query text given by @query@, the input
597+-- serialization settings (compression format and format information)
598+-- @inputSer@ and the output serialization settings @outputSer@.
599+selectRequest :: Text -> InputSerialization -> OutputSerialization
600+ -> SelectRequest
601+selectRequest sqlQuery inputSer outputSer =
602+ SelectRequest { srExpression = sqlQuery
603+ , srExpressionType = SQL
604+ , srInputSerialization = inputSer
605+ , srOutputSerialization = outputSer
606+ , srRequestProgressEnabled = Nothing
607+ }
608+
609+-- | setRequestProgressEnabled sets the flag for turning on progress
610+-- messages when the Select response is being streamed back to the
611+-- client.
612+setRequestProgressEnabled :: Bool -> SelectRequest -> SelectRequest
613+setRequestProgressEnabled enabled sr =
614+ sr { srRequestProgressEnabled = Just enabled }
615+
616+type CSVInputProp = CSVProp
617+
618+-- | CSVProp represents CSV format properties. It is built up using
619+-- the Monoid instance.
620+data CSVProp = CSVProp (H.HashMap Text Text)
621+ deriving (Eq, Show)
622+
623+#if (__GLASGOW_HASKELL__ >= 804)
624+instance Semigroup CSVProp where
625+ (CSVProp a) <> (CSVProp b) = CSVProp (b <> a)
626+#endif
627+
628+instance Monoid CSVProp where
629+ mempty = CSVProp mempty
630+#if (__GLASGOW_HASKELL__ < 804)
631+ mappend (CSVProp a) (CSVProp b) = CSVProp (b <> a)
632+#endif
633+
634+defaultCSVProp :: CSVProp
635+defaultCSVProp = mempty
636+
637+recordDelimiter :: Text -> CSVProp
638+recordDelimiter = CSVProp . H.singleton "RecordDelimiter"
639+
640+fieldDelimiter :: Text -> CSVProp
641+fieldDelimiter = CSVProp . H.singleton "FieldDelimiter"
642+
643+quoteCharacter :: Text -> CSVProp
644+quoteCharacter = CSVProp . H.singleton "QuoteCharacter"
645+
646+quoteEscapeCharacter :: Text -> CSVProp
647+quoteEscapeCharacter = CSVProp . H.singleton "QuoteEscapeCharacter"
648+
649+-- | FileHeaderInfo specifies information about column headers for CSV
650+-- format.
651+data FileHeaderInfo
652+ = FileHeaderNone -- ^ No column headers are present
653+ | FileHeaderUse -- ^ Headers are present and they should be used
654+ | FileHeaderIgnore -- ^ Header are present, but should be ignored
655+ deriving (Eq, Show)
656+
657+fileHeaderInfo :: FileHeaderInfo -> CSVProp
658+fileHeaderInfo = CSVProp . H.singleton "FileHeaderInfo" . toString
659+ where
660+ toString FileHeaderNone = "NONE"
661+ toString FileHeaderUse = "USE"
662+ toString FileHeaderIgnore = "IGNORE"
663+
664+commentCharacter :: Text -> CSVProp
665+commentCharacter = CSVProp . H.singleton "Comments"
666+
667+allowQuotedRecordDelimiter :: CSVProp
668+allowQuotedRecordDelimiter = CSVProp $ H.singleton "AllowQuotedRecordDelimiter" "TRUE"
669+
670+-- | Set the CSV format properties in the InputSerialization.
671+setInputCSVProps :: CSVProp -> InputSerialization -> InputSerialization
672+setInputCSVProps p is = is { isFormatInfo = InputFormatCSV p }
673+
674+-- | Set the CSV format properties in the OutputSerialization.
675+outputCSVFromProps :: CSVProp -> OutputSerialization
676+outputCSVFromProps p = OutputSerializationCSV p
677+
678+data JSONInputProp = JSONInputProp { jsonipType :: JSONType }
679+ deriving (Eq, Show)
680+
681+data JSONType = JSONTypeDocument | JSONTypeLines
682+ deriving (Eq, Show)
683+
684+
685+-- | OutputSerialization represents output serialization settings for
686+-- the SelectRequest. Use `defaultCsvOutput` or `defaultJsonOutput` as
687+-- a starting point.
688+data OutputSerialization = OutputSerializationJSON JSONOutputProp
689+ | OutputSerializationCSV CSVOutputProp
690+ deriving (Eq, Show)
691+
692+type CSVOutputProp = CSVProp
693+
694+-- | quoteFields is an output serialization parameter
695+quoteFields :: QuoteFields -> CSVProp
696+quoteFields q = CSVProp $ H.singleton "QuoteFields" $
697+ case q of
698+ QuoteFieldsAsNeeded -> "ASNEEDED"
699+ QuoteFieldsAlways -> "ALWAYS"
700+
701+data QuoteFields = QuoteFieldsAsNeeded | QuoteFieldsAlways
702+ deriving (Eq, Show)
703+
704+data JSONOutputProp = JSONOutputProp { jsonopRecordDelimiter :: Maybe Text }
705+ deriving (Eq, Show)
706+
707+-- | Set the output record delimiter for JSON format
708+outputJSONFromRecordDelimiter :: Text -> OutputSerialization
709+outputJSONFromRecordDelimiter t =
710+ OutputSerializationJSON (JSONOutputProp $ Just t)
711+
712+-- Response related types
713+
714+-- | An EventMessage represents each kind of message received from the server.
715+data EventMessage = ProgressEventMessage { emProgress :: Progress }
716+ | StatsEventMessage { emStats :: Stats }
717+ | RequestLevelErrorMessage { emErrorCode :: Text
718+ , emErrorMessage :: Text
719+ }
720+ | RecordPayloadEventMessage { emPayloadBytes :: ByteString }
721+ deriving (Eq, Show)
722+
723+data MsgHeaderName = MessageType
724+ | EventType
725+ | ContentType
726+ | ErrorCode
727+ | ErrorMessage
728+ deriving (Eq, Show)
729+
730+msgHeaderValueType :: Word8
731+msgHeaderValueType = 7
732+
733+type MessageHeader = (MsgHeaderName, Text)
734+
735+data Progress = Progress { pBytesScanned :: Int64
736+ , pBytesProcessed :: Int64
737+ , pBytesReturned :: Int64
738+ }
739+ deriving (Eq, Show)
740+
741+type Stats = Progress
742+
743+--------------------------------------------------------------------------
744+-- Select API Related Types End
745+--------------------------------------------------------------------------
746+
514747-- | Represents different kinds of payload that are used with S3 API
515748-- requests.
516749data Payload = PayloadBS ByteString
@@ -530,8 +763,8 @@ data AdminReqInfo = AdminReqInfo {
530763 , ariQueryParams :: Query
531764 }
532765
533−data S3ReqInfo = S3ReqInfo {
534− riMethod :: Method
766+data S3ReqInfo = S3ReqInfo
767+ { riMethod :: Method
535768 , riBucket :: Maybe Bucket
536769 , riObject :: Maybe Object
537770 , riQueryParams :: Query
src/Network/Minio/Errors.hs+1
@@ -51,6 +51,7 @@ data ServiceErr = BucketAlreadyExists
5151 | NoSuchBucket
5252 | InvalidBucketName
5353 | NoSuchKey
54+ | SelectErr Text Text
5455 | ServiceErr Text Text
5556 deriving (Show, Eq)
5657
src/Network/Minio/SelectAPI.hsnew file+302
@@ -0,0 +1,302 @@
1+--
2+-- Minio Haskell SDK, (C) 2017-2019 Minio, Inc.
3+--
4+-- Licensed under the Apache License, Version 2.0 (the "License");
5+-- you may not use this file except in compliance with the License.
6+-- You may obtain a copy of the License at
7+--
8+-- http://www.apache.org/licenses/LICENSE-2.0
9+--
10+-- Unless required by applicable law or agreed to in writing, software
11+-- distributed under the License is distributed on an "AS IS" BASIS,
12+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+-- See the License for the specific language governing permissions and
14+-- limitations under the License.
15+--
16+
17+module 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+
34+ , InputSerialization
35+ , defaultCsvInput
36+ , linesJsonInput
37+ , documentJsonInput
38+ , defaultParquetInput
39+ , setInputCSVProps
40+
41+ , CompressionType(..)
42+ , setInputCompressionType
43+
44+ -- *** CSV Format details
45+ ------------------------
46+ -- | CSV format options such as delimiters and quote characters are
47+ -- specified using using the functions below. Options are combined
48+ -- monoidally.
49+
50+ , CSVProp
51+ , recordDelimiter
52+ , fieldDelimiter
53+ , quoteCharacter
54+ , quoteEscapeCharacter
55+ , commentCharacter
56+ , allowQuotedRecordDelimiter
57+ , FileHeaderInfo(..)
58+ , fileHeaderInfo
59+ , QuoteFields(..)
60+ , quoteFields
61+
62+ -- *** Output Serialization
63+ -------------------------
64+
65+ , OutputSerialization
66+ , defaultCsvOutput
67+ , defaultJsonOutput
68+ , outputCSVFromProps
69+ , outputJSONFromRecordDelimiter
70+
71+ -- *** Progress messages
72+ ------------------------
73+
74+ , setRequestProgressEnabled
75+
76+ -- *** Interpreting Select output
77+ --------------------------------------------
78+ -- | The conduit returned by `selectObjectContent` returns values of
79+ -- the `EventMessage` data type. This returns the query output
80+ -- messages formatted according to the chosen output serialization,
81+ -- interleaved with progress messages (if enabled by
82+ -- `setRequestProgressEnabled`), and at the end a statistics
83+ -- message.
84+ --
85+ -- If the application is interested in only the payload, then
86+ -- `getPayloadBytes` can be used. For example to simply print the
87+ -- payload to stdout:
88+ --
89+ -- > resultConduit <- selectObjectContent bucket object mySelectRequest
90+ -- > runConduit $ resultConduit .| getPayloadBytes .| stdoutC
91+ --
92+ -- Note that runConduit, the connect operator (.|) and stdoutC are
93+ -- all from the "conduit" package.
94+
95+ , getPayloadBytes
96+ , EventMessage(..)
97+ , Progress(..)
98+ , Stats
99+ ) where
100+
101+import Conduit ((.|))
102+import qualified Conduit as C
103+import qualified Data.Binary as Bin
104+import qualified Data.ByteString as B
105+import qualified Data.ByteString.Lazy as LB
106+import Data.Digest.CRC32 (crc32, crc32Update)
107+import qualified Network.HTTP.Conduit as NC
108+import qualified Network.HTTP.Types as HT
109+import UnliftIO (MonadUnliftIO)
110+
111+import Lib.Prelude
112+
113+import Network.Minio.API
114+import Network.Minio.Data
115+import Network.Minio.Errors
116+import Network.Minio.Utils
117+import Network.Minio.XmlGenerator
118+import Network.Minio.XmlParser
119+
120+data EventStreamException = ESEPreludeCRCFailed
121+ | ESEMessageCRCFailed
122+ | ESEUnexpectedEndOfStream
123+ | ESEDecodeFail [Char]
124+ | ESEInvalidHeaderType
125+ | ESEInvalidHeaderValueType
126+ | ESEInvalidMessageType
127+ deriving (Eq, Show)
128+
129+instance Exception EventStreamException
130+
131+-- chunkSize in bytes is 32KiB
132+chunkSize :: Int
133+chunkSize = 32 * 1024
134+
135+parseBinary :: Bin.Binary a => ByteString -> IO a
136+parseBinary b = do
137+ case Bin.decodeOrFail $ LB.fromStrict b of
138+ Left (_, _, msg) -> throwIO $ ESEDecodeFail msg
139+ Right (_, _, r) -> return r
140+
141+bytesToHeaderName :: Text -> IO MsgHeaderName
142+bytesToHeaderName t = case t of
143+ ":message-type" -> return MessageType
144+ ":event-type" -> return EventType
145+ ":content-type" -> return ContentType
146+ ":error-code" -> return ErrorCode
147+ ":error-message" -> return ErrorMessage
148+ _ -> throwIO ESEInvalidHeaderType
149+
150+parseHeaders :: MonadUnliftIO m
151+ => Word32 -> C.ConduitM ByteString a m [MessageHeader]
152+parseHeaders 0 = return []
153+parseHeaders hdrLen = do
154+ bs1 <- readNBytes 1
155+ n :: Word8 <- liftIO $ parseBinary bs1
156+
157+ headerKeyBytes <- readNBytes $ fromIntegral n
158+ let headerKey = decodeUtf8Lenient headerKeyBytes
159+ headerName <- liftIO $ bytesToHeaderName headerKey
160+
161+ bs2 <- readNBytes 1
162+ headerValueType :: Word8 <- liftIO $ parseBinary bs2
163+ when (headerValueType /= 7) $ throwIO ESEInvalidHeaderValueType
164+
165+ bs3 <- readNBytes 2
166+ vLen :: Word16 <- liftIO $ parseBinary bs3
167+ headerValueBytes <- readNBytes $ fromIntegral vLen
168+ let headerValue = decodeUtf8Lenient headerValueBytes
169+ m = (headerName, headerValue)
170+ k = 1 + fromIntegral n + 1 + 2 + fromIntegral vLen
171+
172+ ms <- parseHeaders (hdrLen - k)
173+ return (m:ms)
174+
175+-- readNBytes returns N bytes read from the string and throws an
176+-- exception if N bytes are not present on the stream.
177+readNBytes :: MonadUnliftIO m => Int -> C.ConduitM ByteString a m ByteString
178+readNBytes n = do
179+ b <- LB.toStrict <$> (C.takeCE n .| C.sinkLazy)
180+ if B.length b /= n
181+ then throwIO ESEUnexpectedEndOfStream
182+ else return b
183+
184+crcCheck :: MonadUnliftIO m
185+ => C.ConduitM ByteString ByteString m ()
186+crcCheck = do
187+ b <- readNBytes 12
188+ n :: Word32 <- liftIO $ parseBinary $ B.take 4 b
189+ preludeCRC :: Word32 <- liftIO $ parseBinary $ B.drop 8 b
190+ when (crc32 (B.take 8 b) /= preludeCRC) $
191+ throwIO ESEPreludeCRCFailed
192+
193+ -- we do not yield the checksum
194+ C.yield $ B.take 8 b
195+
196+ -- 12 bytes have been read off the current message. Now read the
197+ -- next (n-12)-4 bytes and accumulate the checksum, and yield it.
198+ let startCrc = crc32 b
199+ finalCrc <- accumulateYield (fromIntegral n-16) startCrc
200+
201+ bs <- readNBytes 4
202+ expectedCrc :: Word32 <- liftIO $ parseBinary bs
203+
204+ when (finalCrc /= expectedCrc) $
205+ throwIO ESEMessageCRCFailed
206+
207+ -- we unconditionally recurse - downstream figures out when to
208+ -- quit reading the stream
209+ crcCheck
210+ where
211+ accumulateYield n checkSum = do
212+ let toRead = min n chunkSize
213+ b <- readNBytes toRead
214+ let c' = crc32Update checkSum b
215+ n' = n - B.length b
216+ C.yield b
217+ if n' > 0
218+ then accumulateYield n' c'
219+ else return c'
220+
221+handleMessage :: MonadUnliftIO m => C.ConduitT ByteString EventMessage m ()
222+handleMessage = do
223+ b1 <- readNBytes 4
224+ msgLen :: Word32 <- liftIO $ parseBinary b1
225+
226+ b2 <- readNBytes 4
227+ hdrLen :: Word32 <- liftIO $ parseBinary b2
228+
229+ hs <- parseHeaders hdrLen
230+
231+ let payloadLen = msgLen - hdrLen - 16
232+ getHdrVal h = fmap snd . headMay . filter ((h ==) . fst)
233+ eventHdrValue = getHdrVal EventType hs
234+ msgHdrValue = getHdrVal MessageType hs
235+ errCode = getHdrVal ErrorCode hs
236+ errMsg = getHdrVal ErrorMessage hs
237+
238+ case msgHdrValue of
239+ Just "event" -> do
240+ case eventHdrValue of
241+ Just "Records" -> passThrough $ fromIntegral payloadLen
242+ Just "Cont" -> return ()
243+ Just "Progress" -> do
244+ bs <- readNBytes $ fromIntegral payloadLen
245+ progress <- parseSelectProgress bs
246+ C.yield $ ProgressEventMessage progress
247+ Just "Stats" -> do
248+ bs <- readNBytes $ fromIntegral payloadLen
249+ stats <- parseSelectProgress bs
250+ C.yield $ StatsEventMessage stats
251+ Just "End" -> return ()
252+ _ -> throwIO ESEInvalidMessageType
253+ when (eventHdrValue /= Just "End") handleMessage
254+
255+ Just "error" -> do
256+ let reqMsgMay = RequestLevelErrorMessage <$> errCode <*> errMsg
257+ maybe (throwIO ESEInvalidMessageType) C.yield reqMsgMay
258+
259+ _ -> throwIO ESEInvalidMessageType
260+
261+ where
262+ passThrough 0 = return ()
263+ passThrough n = do
264+ let c = min n chunkSize
265+ b <- readNBytes c
266+ C.yield $ RecordPayloadEventMessage b
267+ passThrough $ n - B.length b
268+
269+
270+selectProtoConduit :: MonadUnliftIO m
271+ => C.ConduitT ByteString EventMessage m ()
272+selectProtoConduit = crcCheck .| handleMessage
273+
274+-- | selectObjectContent calls the SelectRequest on the given
275+-- object. It returns a Conduit of event messages that can be consumed
276+-- by the client.
277+selectObjectContent :: Bucket -> Object -> SelectRequest
278+ -> Minio (C.ConduitT () EventMessage Minio ())
279+selectObjectContent b o r = do
280+ let reqInfo = defaultS3ReqInfo { riMethod = HT.methodPost
281+ , riBucket = Just b
282+ , riObject = Just o
283+ , riPayload = PayloadBS $ mkSelectRequest r
284+ , riNeedsLocation = False
285+ , riQueryParams = [("select", Nothing), ("select-type", Just "2")]
286+ }
287+ --print $ mkSelectRequest r
288+ resp <- mkStreamRequest reqInfo
289+ return $ NC.responseBody resp .| selectProtoConduit
290+
291+-- | A helper conduit that returns only the record payload bytes.
292+getPayloadBytes :: MonadIO m => C.ConduitT EventMessage ByteString m ()
293+getPayloadBytes = do
294+ evM <- C.await
295+ case evM of
296+ Just v -> do
297+ case v of
298+ RecordPayloadEventMessage b -> C.yield b
299+ RequestLevelErrorMessage c m -> liftIO $ throwIO $ SelectErr c m
300+ _ -> return ()
301+ getPayloadBytes
302+ Nothing -> return ()
src/Network/Minio/XmlGenerator.hs+71−8
@@ -18,11 +18,12 @@ module Network.Minio.XmlGenerator
1818 ( mkCreateBucketConfig
1919 , mkCompleteMultipartUploadRequest
2020 , mkPutNotificationRequest
21+ , mkSelectRequest
2122 ) where
2223
2324
2425import qualified Data.ByteString.Lazy as LBS
25−import qualified Data.Map as M
26+import qualified Data.HashMap.Strict as H
2627import qualified Data.Text as T
2728import Text.XML
2829
@@ -35,7 +36,7 @@ import Network.Minio.Data
3536mkCreateBucketConfig :: Text -> Region -> ByteString
3637mkCreateBucketConfig ns location = LBS.toStrict $ renderLBS def bucketConfig
3738 where
38− s3Element n = Element (s3Name ns n) M.empty
39+ s3Element n = Element (s3Name ns n) mempty
3940 root = s3Element "CreateBucketConfiguration"
4041 [ NodeElement $ s3Element "LocationConstraint"
4142 [ NodeContent location]
@@ -47,12 +48,12 @@ mkCompleteMultipartUploadRequest :: [PartTuple] -> ByteString
4748mkCompleteMultipartUploadRequest partInfo =
4849 LBS.toStrict $ renderLBS def cmur
4950 where
50− root = Element "CompleteMultipartUpload" M.empty $
51+ root = Element "CompleteMultipartUpload" mempty $
5152 map (NodeElement . mkPart) partInfo
52− mkPart (n, etag) = Element "Part" M.empty
53− [ NodeElement $ Element "PartNumber" M.empty
53+ mkPart (n, etag) = Element "Part" mempty
54+ [ NodeElement $ Element "PartNumber" mempty
5455 [NodeContent $ T.pack $ show n]
55− , NodeElement $ Element "ETag" M.empty
56+ , NodeElement $ Element "ETag" mempty
5657 [NodeContent etag]
5758 ]
5859 cmur = Document (Prologue [] Nothing []) root []
@@ -67,9 +68,9 @@ toXML ns node = LBS.toStrict $ renderLBS def $
6768 Document (Prologue [] Nothing []) (xmlNode node) []
6869 where
6970 xmlNode :: XNode -> Element
70− xmlNode (XNode name nodes) = Element (s3Name ns name) M.empty $
71+ xmlNode (XNode name nodes) = Element (s3Name ns name) mempty $
7172 map (NodeElement . xmlNode) nodes
72− xmlNode (XLeaf name content) = Element (s3Name ns name) M.empty
73+ xmlNode (XLeaf name content) = Element (s3Name ns name) mempty
7374 [NodeContent content]
7475
7576class ToXNode a where
@@ -100,3 +101,65 @@ getFRXNode (FilterRule n v) = XNode "FilterRule" [ XLeaf "Name" n
100101
101102mkPutNotificationRequest :: Text -> Notification -> ByteString
102103mkPutNotificationRequest ns = toXML ns . toXNode
104+
105+mkSelectRequest :: SelectRequest -> ByteString
106+mkSelectRequest r = LBS.toStrict $ renderLBS def sr
107+ where
108+ 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 $
133+ if | c == CompressionTypeNone -> "NONE"
134+ | c == CompressionTypeGzip -> "GZIP"
135+ | c == CompressionTypeBzip2 -> "BZIP2"
136+ ]
137+ ]
138+ comprTypeNode Nothing = []
139+
140+ kvElement (k, v) = Element (Name k Nothing Nothing) mempty [NodeContent v]
141+ formatNode (InputFormatCSV (CSVProp h)) =
142+ Element "CSV" mempty
143+ (map NodeElement $ map kvElement $ H.toList h)
144+ formatNode (InputFormatJSON p) =
145+ Element "JSON" mempty
146+ [NodeElement
147+ (Element "Type" mempty
148+ [NodeContent $
149+ if | jsonipType p == JSONTypeDocument -> "DOCUMENT"
150+ | jsonipType p == JSONTypeLines -> "LINES"
151+ ]
152+ )
153+ ]
154+ formatNode InputFormatParquet = Element "Parquet" mempty []
155+
156+ outputSerializationNodes (OutputSerializationJSON j) =
157+ [NodeElement (Element "JSON" mempty $
158+ rdElem $ jsonopRecordDelimiter j)]
159+ outputSerializationNodes (OutputSerializationCSV (CSVProp h)) =
160+ [NodeElement $ Element "CSV" mempty
161+ (map NodeElement $ map kvElement $ H.toList h)]
162+
163+ rdElem Nothing = []
164+ rdElem (Just t) = [NodeElement $ Element "RecordDelimiter" mempty
165+ [NodeContent t]]
src/Network/Minio/XmlParser.hs+12
@@ -26,8 +26,10 @@ module Network.Minio.XmlParser
2626 , parseListPartsResponse
2727 , parseErrResponse
2828 , parseNotification
29+ , parseSelectProgress
2930 ) where
3031
32+import qualified Data.ByteString.Lazy as LB
3133import Data.List (zip3, zip4, zip5)
3234import qualified Data.Map as Map
3335import qualified Data.Text as T
@@ -261,3 +263,13 @@ parseNotification xmldata = do
261263 s3Elem ns "FilterRule" &| getFilterRule ns
262264 return $ NotificationConfig id arn events
263265 (Filter $ FilterKey $ FilterRules rules)
266+
267+parseSelectProgress :: MonadIO m => ByteString -> m Progress
268+parseSelectProgress xmldata = do
269+ r <- parseRoot $ LB.fromStrict xmldata
270+ let bScanned = T.concat $ r $/ element "BytesScanned" &/ content
271+ bProcessed = T.concat $ r $/element "BytesProcessed" &/ content
272+ bReturned = T.concat $ r $/element "BytesReturned" &/ content
273+ Progress <$> parseDecimal bScanned
274+ <*> parseDecimal bProcessed
275+ <*> parseDecimal bReturned
stack.yaml+1−1
@@ -15,7 +15,7 @@
1515# resolver:
1616# name: custom-snapshot
1717# location: "./custom-snapshot.yaml"
18−resolver: lts-11.1
18+resolver: lts-13.1
1919
2020# User packages to be built.
2121# Various formats can be used as shown in the example below.
test/Network/Minio/XmlGenerator/Test.hs+46
@@ -14,12 +14,14 @@
1414-- limitations under the License.
1515--
1616
17+{-# LANGUAGE QuasiQuotes #-}
1718module Network.Minio.XmlGenerator.Test
1819 ( xmlGeneratorTests
1920 ) where
2021
2122import Test.Tasty
2223import Test.Tasty.HUnit
24+import Text.RawString.QQ (r)
2325
2426import Lib.Prelude
2527
@@ -33,6 +35,7 @@ xmlGeneratorTests = testGroup "XML Generator Tests"
3335 [ testCase "Test mkCreateBucketConfig" testMkCreateBucketConfig
3436 , testCase "Test mkCompleteMultipartUploadRequest" testMkCompleteMultipartUploadRequest
3537 , testCase "Test mkPutNotificationRequest" testMkPutNotificationRequest
38+ , testCase "Test mkSelectRequest" testMkSelectRequest
3639 ]
3740
3841testMkCreateBucketConfig :: Assertion
@@ -95,3 +98,46 @@ testMkPutNotificationRequest =
9598 [ObjectCreated] defaultFilter
9699 ]
97100 ]
101+
102+testMkSelectRequest :: Assertion
103+testMkSelectRequest = mapM_ assertFn cases
104+ where
105+ 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 "\""
119+ )
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>
122+</RecordDelimiter><FileHeaderInfo>IGNORE</FileHeaderInfo><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></InputSerialization><OutputSerialization><CSV><QuoteCharacter>&#34;</QuoteCharacter><QuoteFields>ASNEEDED</QuoteFields><RecordDelimiter>
123+</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>
130+</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>
141+</RecordDelimiter><QuoteEscapeCharacter>&#34;</QuoteEscapeCharacter><FieldDelimiter>,</FieldDelimiter></CSV></OutputSerialization><RequestProgress><Enabled>FALSE</Enabled></RequestProgress></SelectRequest>|]
142+ )
143+ ]
test/Network/Minio/XmlParser/Test.hs+25−2
@@ -14,15 +14,16 @@
1414-- limitations under the License.
1515--
1616
17+{-# LANGUAGE QuasiQuotes #-}
1718module Network.Minio.XmlParser.Test
18− (
19− xmlParserTests
19+ ( xmlParserTests
2020 ) where
2121
2222import qualified Data.Map as Map
2323import Data.Time (fromGregorian)
2424import Test.Tasty
2525import Test.Tasty.HUnit
26+import Text.RawString.QQ (r)
2627import UnliftIO (MonadUnliftIO)
2728
2829import Lib.Prelude
@@ -43,6 +44,7 @@ xmlParserTests = testGroup "XML Parser Tests"
4344 , testCase "Test parseListPartsResponse" testParseListPartsResponse
4445 , testCase "Test parseCopyObjectResponse" testParseCopyObjectResponse
4546 , testCase "Test parseNotification" testParseNotification
47+ , testCase "Test parseSelectProgress" testParseSelectProgress
4648 ]
4749
4850tryValidationErr :: (MonadUnliftIO m) => m a -> m (Either MErrV a)
@@ -356,3 +358,24 @@ testParseNotification = do
356358 forM_ cases $ \(xmldata, val) -> do
357359 result <- runExceptT $ runTestNS $ parseNotification xmldata
358360 eitherValidationErr result (@?= val)
361+
362+-- | Tests parsing of both progress and stats
363+testParseSelectProgress :: Assertion
364+testParseSelectProgress = do
365+ let cases = [ ([r|<?xml version="1.0" encoding="UTF-8"?>
366+<Progress>
367+ <BytesScanned>512</BytesScanned>
368+ <BytesProcessed>1024</BytesProcessed>
369+ <BytesReturned>1024</BytesReturned>
370+</Progress>|] , Progress 512 1024 1024)
371+ , ([r|<?xml version="1.0" encoding="UTF-8"?>
372+<Stats>
373+ <BytesScanned>512</BytesScanned>
374+ <BytesProcessed>1024</BytesProcessed>
375+ <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)