on develop help sign in
Adding multipart/related support (presently only for chunked upload)
commit3b4095f60fc4694ebebce8fb7433aabe0719dca7 authorBrendan Hay <brendan.g.hay@gmail.com> dateSat 17 Oct 2015, 17:38 (11 years ago) parent427533b
4 files changed+170−96
core/src/Network/Google/Types.hs+53−78 gogol/gogol.cabal+4−1 gogol/src/Network/Google/Internal/HTTP.hs+37−17 gogol/src/Network/Google/Internal/Multipart.hs+76
core/src/Network/Google/Types.hs+53−78
@@ -23,35 +23,35 @@
2323module Network.Google.Types where
2424
2525import Control.Applicative
26−import Control.Exception.Lens (exception)
27−import Control.Lens hiding (coerce)
26+import Control.Exception.Lens (exception)
27+import Control.Lens hiding (coerce)
2828import Control.Monad
2929import Control.Monad.Catch
3030import Control.Monad.Trans.Resource
3131import Data.Aeson
32−import Data.ByteString (ByteString)
33−import qualified Data.ByteString.Char8 as BS8
34−import qualified Data.ByteString.Lazy as LBS
35−import qualified Data.CaseInsensitive as CI
32+import Data.ByteString (ByteString)
33+import qualified Data.ByteString.Char8 as BS8
34+import qualified Data.ByteString.Lazy as LBS
35+import qualified Data.CaseInsensitive as CI
3636import Data.Coerce
3737import Data.Conduit
38−import qualified Data.Conduit.List as CL
38+import qualified Data.Conduit.List as CL
3939import Data.Data
40−import Data.DList (DList)
41−import qualified Data.DList as DList
42−import Data.Foldable (foldl')
40+import Data.DList (DList)
41+import qualified Data.DList as DList
42+import Data.Foldable (foldl')
4343import Data.Monoid
4444import Data.String
45−import Data.Text (Text)
46−import qualified Data.Text.Encoding as Text
47−import Data.Text.Lazy.Builder (Builder)
48−import qualified Data.Text.Lazy.Builder as Build
45+import Data.Text (Text)
46+import qualified Data.Text.Encoding as Text
47+import Data.Text.Lazy.Builder (Builder)
48+import qualified Data.Text.Lazy.Builder as Build
4949import GHC.Generics
5050import GHC.TypeLits
51−import Network.HTTP.Client (HttpException)
52−import Network.HTTP.Media hiding (Accept)
53−import Network.HTTP.Types hiding (Header)
54−import qualified Network.HTTP.Types as HTTP
51+import Network.HTTP.Client (HttpException)
52+import Network.HTTP.Media hiding (Accept)
53+import Network.HTTP.Types hiding (Header)
54+import qualified Network.HTTP.Types as HTTP
5555import Servant.API
5656
5757data AltJSON = AltJSON
@@ -199,21 +199,28 @@ serviceSecure = lens _svcSecure (\s a -> s { _svcSecure = a })
199199serviceTimeout :: Lens' Service (Maybe Seconds)
200200serviceTimeout = lens _svcTimeout (\s a -> s { _svcTimeout = a })
201201
202+-- | A single part of a multipart message.
203+data Part = Part MediaType [(HeaderName, ByteString)] Body
204+
205+data Payload
206+ = Body !MediaType !Body
207+ | Related ![Part]
208+
202209-- | An intermediary request builder.
203210data Request = Request
204− { _rqPath :: Builder
205− , _rqQuery :: DList (ByteString, Maybe ByteString)
206− , _rqHeaders :: DList (HeaderName, ByteString)
207− , _rqBody :: Maybe (MediaType, Body)
211+ { _rqPath :: !Builder
212+ , _rqQuery :: !(DList (ByteString, Maybe ByteString))
213+ , _rqHeaders :: !(DList (HeaderName, ByteString))
214+ , _rqBody :: !(Maybe Payload)
208215 }
209216
210217instance Monoid Request where
211218 mempty = Request mempty mempty mempty Nothing
212219 mappend a b = Request
213− (_rqPath a <> "/" <> _rqPath b)
214− (_rqQuery a <> _rqQuery b)
215− (_rqHeaders a <> _rqHeaders b)
216− (_rqBody b <|> _rqBody a)
220+ (_rqPath a <> "/" <> _rqPath b)
221+ (_rqQuery a <> _rqQuery b)
222+ (_rqHeaders a <> _rqHeaders b)
223+ (_rqBody b <|> _rqBody a)
217224
218225appendPath :: Request -> Builder -> Request
219226appendPath rq x = rq { _rqPath = _rqPath rq <> "/" <> x }
@@ -233,16 +240,19 @@ appendHeader rq k (Just v) = rq
233240 }
234241
235242setBody :: Request -> MediaType -> Body -> Request
236−setBody rq c x = rq { _rqBody = Just (c, x) }
243+setBody rq c x = rq { _rqBody = Just (Body c x) }
244+
245+setRelated :: Request -> [Part] -> Request
246+setRelated rq ps = rq { _rqBody = Just (Related ps) }
237247
238248-- | A materialised 'http-client' request and associated response parser.
239249data Client a = Client
240− { _cliAccept :: Maybe MediaType
241− , _cliMethod :: Method
242− , _cliCheck :: Status -> Bool
243− , _cliService :: Service
244− , _cliRequest :: Request
245− , _cliResponse :: Stream -> ResourceT IO (Either String a)
250+ { _cliAccept :: !(Maybe MediaType)
251+ , _cliMethod :: !Method
252+ , _cliCheck :: !(Status -> Bool)
253+ , _cliService :: !Service
254+ , _cliRequest :: !Request
255+ , _cliResponse :: !(Stream -> ResourceT IO (Either String a))
246256 }
247257
248258clientService :: Lens' (Client a) Service
@@ -321,55 +331,20 @@ data CaptureMode (s :: Symbol) (m :: Symbol) a
321331data MultipartRelated (cs :: [*]) m b
322332 deriving (Typeable)
323333
324−instance ( MimeRender c m
334+instance ( ToBody c m
335+ , ToBody OctetStream b
325336 , GoogleClient fn
326337 ) => GoogleClient (MultipartRelated (c ': cs) m b :> fn) where
327338 type Fn (MultipartRelated (c ': cs) m b :> fn) = m -> b -> Fn fn
328339
329− -- requestBuild Proxy rq m b = requestBuild (Proxy :: Proxy fn) $
330− -- mimeRender p m
331− -- mimeRender (Proxy :: Proxy OctetStream) b
332− -- where
333− -- p = Proxy :: Proxy c
334−
335−-- POST /upload/drive/v2/files?uploadType=multipart HTTP/1.1
336−-- Host: www.googleapis.com
337−-- Authorization: Bearer your_auth_token
338−-- Content-Type: multipart/related; boundary="foo_bar_baz"
339−-- Content-Length: number_of_bytes_in_entire_request_body
340−--
341−-- --foo_bar_baz
342−-- Content-Type: application/json; charset=UTF-8
343−-- {
344−-- "title": "My File"
345−-- }
346−-- --foo_bar_baz
347−-- Content-Type: image/jpeg
348−-- JPEG data
349−-- --foo_bar_baz--
350−
351−-- "mediaType" param also comes/calculated from the request body?
352−--
353−-- Resumable endpoints - Not supported to begin with, need to figure
354−-- out how to return session id etc.
355−--
356−-- Assume initially that every service supports multipart upload.
357−--
358−-- When downloading media you must set the alt query parameter
359−-- to media in the request URL.
360−--
361−-- "resumable" or "multipart" needs to go into the "uploadType" param
362−--
363−
364−-- if c.protocol_ == "resumable" {
365−-- if c.mediaType_ == "" {
366−-- c.mediaType_ = googleapi.DetectMediaType(c.resumable_)
367−-- }
368−-- req.Header.Set("X-Upload-Content-Type", c.mediaType_)
369−-- req.Header.Set("Content-Type", "application/json; charset=utf-8")
370−-- } else {
371−-- req.Header.Set("Content-Type", ctype)
372−-- }
340+ buildClient Proxy rq m b = buildClient (Proxy :: Proxy fn) $
341+ setRelated rq
342+ [ Part (contentType mc) [] (toBody mc m)
343+ , Part (contentType mb) [] (toBody mb b)
344+ ]
345+ where
346+ mc = Proxy :: Proxy c
347+ mb = Proxy :: Proxy OctetStream
373348
374349instance (KnownSymbol s, GoogleClient fn) => GoogleClient (s :> fn) where
375350 type Fn (s :> fn) = Fn fn
gogol/gogol.cabal+4−1
@@ -33,6 +33,7 @@ library
3333
3434 other-modules:
3535 Network.Google.Internal.HTTP
36+ , Network.Google.Internal.Multipart
3637
3738 build-depends:
3839 aeson >= 0.8
@@ -54,4 +55,6 @@ library
5455 , time >= 1.2
5556 , transformers >= 0.2
5657 , transformers-base >= 0.4
57− , unordered-containers >= 0.2.5
58+ , unordered-containers >= 0.2.5
59+ , http-client
60+ , case-insensitive
gogol/src/Network/Google/Internal/HTTP.hs+37−17
@@ -17,17 +17,30 @@ import Control.Monad.Catch
1717import Control.Monad.Trans.Resource
1818import Data.Default.Class
1919import Data.Monoid
20−import qualified Data.Text.Encoding as Text
21−import qualified Data.Text.Lazy as LText
22−import qualified Data.Text.Lazy.Builder as Build
23−import GHC.Exts (toList)
24−import Network.Google.Env (Env (..))
20+import qualified Data.Text.Encoding as Text
21+import qualified Data.Text.Lazy as LText
22+import qualified Data.Text.Lazy.Builder as Build
23+import GHC.Exts (toList)
24+import Network.Google.Env (Env (..))
25+import Network.Google.Internal.Multipart
2526import Network.Google.Types
26−import qualified Network.HTTP.Client.Conduit as Client
27+import qualified Network.HTTP.Client.Conduit as Client
2728import Network.HTTP.Conduit
2829import Network.HTTP.Media
2930import Network.HTTP.Types
3031
32+-- FIXME: "mediaType" param also comes/calculated from the request body?
33+--
34+-- Resumable endpoints - Not supported to begin with, need to figure
35+-- out how to return session id etc.
36+--
37+-- Assume initially that every service supports multipart upload.
38+--
39+-- When downloading media you must set the alt query parameter
40+-- to media in the request URL.
41+--
42+-- "resumable" or "multipart" needs to go into the "uploadType" param
43+
3144perform :: (MonadCatch m, MonadResource m, GoogleRequest a)
3245 => Env
3346 -> a
@@ -41,8 +54,9 @@ perform Env{..} x = catches go handlers
4154 & clientService %~ appEndo (getDual _envOverride)
4255
4356 go = liftResourceT $ do
44− rs <- http rq _envManager
45− r <- _cliResponse (responseBody rs)
57+ (ct, b) <- content
58+ rs <- http (rq ct b) _envManager
59+ r <- _cliResponse (responseBody rs)
4660 pure $! case r of
4761 Right y -> Right y
4862 Left e -> Left . SerializeError $ SerializeError'
@@ -52,7 +66,7 @@ perform Env{..} x = catches go handlers
5266 , _serializeMessage = e
5367 }
5468
55− rq = def
69+ rq ct b = def
5670 { Client.host = _svcHost
5771 , Client.port = _svcPort
5872 , Client.secure = _svcSecure
@@ -61,20 +75,26 @@ perform Env{..} x = catches go handlers
6175 , Client.method = _cliMethod
6276 , Client.path = path
6377 , Client.queryString = renderQuery True (toList _rqQuery)
64− , Client.requestHeaders = accept (content (toList _rqHeaders))
65− , Client.requestBody = body
78+ , Client.requestHeaders = accept (ct (toList _rqHeaders))
79+ , Client.requestBody = b
6680 }
6781
6882 accept
6983 | Just t <- _cliAccept = ((hAccept, renderHeader t) :)
7084 | otherwise = id
7185
72− content
73− | Just (t, _) <- _rqBody = ((hContentType, renderHeader t) :)
74− | otherwise = id
75−
76− body | Just (_, b) <- _rqBody = Client.requestBodySourceChunked b
77− | otherwise = mempty
86+ content =
87+ case _rqBody of
88+ Nothing -> pure (id, mempty)
89+ Just (Body t s) -> pure
90+ ( ((hContentType, renderHeader t) :)
91+ , Client.requestBodySourceChunked s
92+ )
93+ Just (Related ps) -> do
94+ b <- genBoundary
95+ pure ( (multipartHeader b :)
96+ , renderParts b ps
97+ )
7898
7999 path = Text.encodeUtf8
80100 . LText.toStrict
gogol/src/Network/Google/Internal/Multipart.hsnew file+76
@@ -0,0 +1,76 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+-- |
4+-- Module : Network.Google.Internal.Multipart
5+-- Copyright : (c) 2015 Brendan Hay
6+-- License : Mozilla Public License, v. 2.0.
7+-- Maintainer : Brendan Hay <brendan.g.hay@gmail.com>
8+-- Stability : provisional
9+-- Portability : non-portable (GHC extensions)
10+--
11+module Network.Google.Internal.Multipart where
12+
13+import Control.Monad.IO.Class
14+import Data.ByteString (ByteString)
15+import qualified Data.ByteString as BS
16+import Data.ByteString.Builder.Extra
17+import qualified Data.CaseInsensitive as CI
18+import Data.Monoid
19+import Network.Google.Types
20+import Network.HTTP.Client
21+import qualified Network.HTTP.Client.Conduit as Client
22+import Network.HTTP.Client.MultipartFormData (webkitBoundary)
23+import Network.HTTP.Media
24+import Network.HTTP.Types
25+
26+-- POST /upload/drive/v2/files?uploadType=multipart HTTP/1.1
27+-- Host: www.googleapis.com
28+-- Authorization: Bearer your_auth_token
29+-- Content-Type: multipart/related; boundary="foo_bar_baz"
30+-- Content-Length: number_of_bytes_in_entire_request_body
31+
32+-- --foo_bar_baz
33+-- Content-Type: application/json; charset=UTF-8N
34+-- {
35+-- "title": "My File"
36+-- }
37+-- --foo_bar_baz
38+-- Content-Type: image/jpeg
39+-- JPEG data
40+-- --foo_bar_baz--
41+
42+newtype Boundary = Boundary ByteString
43+
44+genBoundary :: MonadIO m => m Boundary
45+genBoundary = Boundary <$> liftIO webkitBoundary
46+
47+multipartHeader :: Boundary -> Header
48+multipartHeader (Boundary bs) =
49+ ( hContentType
50+ , "multipart/related; boundary=\"" <> bs <> "\""
51+ )
52+
53+start :: Boundary -> RequestBody
54+start (Boundary bs) = copy "--" <> copy bs <> copy "\r\n"
55+
56+part :: Boundary -> RequestBody
57+part (Boundary bs) = copy "--" <> copy bs <> copy "--\r\n"
58+
59+copy :: ByteString -> RequestBody
60+copy bs = RequestBodyBuilder (fromIntegral (BS.length bs)) (byteStringCopy bs)
61+
62+renderParts :: Boundary -> [Part] -> RequestBody
63+renderParts b = (<> part b) . foldMap go
64+ where
65+ go (Part ct hs x) =
66+ start b
67+ <> copy "Content-Type: "
68+ <> copy (renderHeader ct)
69+ <> foldMap (\(k, v) ->
70+ copy "\r\n"
71+ <> copy (CI.original k)
72+ <> copy ": "
73+ <> copy v) hs
74+ <> copy "\r\n\r\n"
75+ <> Client.requestBodySourceChunked x
76+ <> copy "\r\n"