1
1
{-# LANGUAGE CPP #-}
2
+ {-# LANGUAGE AllowAmbiguousTypes #-}
2
3
{-# LANGUAGE DataKinds #-}
3
4
{-# LANGUAGE TypeFamilies #-}
4
5
{-# LANGUAGE InstanceSigs #-}
19
20
-- an API. See haddocks of 'MultipartForm' for an introduction.
20
21
module Servant.Multipart
21
22
( MultipartForm
23
+ , MultipartForm'
22
24
, MultipartData (.. )
23
25
, FromMultipart (.. )
24
26
, lookupInput
@@ -45,20 +47,21 @@ import Control.Monad (replicateM)
45
47
import Control.Monad.IO.Class
46
48
import Control.Monad.Trans.Resource
47
49
import Data.Array (listArray , (!) )
48
- import Data.Foldable (foldMap , foldl' )
49
- import Data.List (find )
50
+ import Data.List (find , foldl' )
50
51
import Data.Maybe
51
52
import Data.Monoid
53
+ import Data.String.Conversions (cs )
52
54
import Data.Text (Text , unpack )
53
55
import Data.Text.Encoding (decodeUtf8 , encodeUtf8 )
54
56
import Data.Typeable
55
57
import Network.HTTP.Media.MediaType ((//) , (/:) )
56
58
import Network.Wai
57
59
import Network.Wai.Parse
58
- import Servant
60
+ import Servant hiding (contentType )
61
+ import Servant.API.Modifiers (FoldLenient )
59
62
import Servant.Client.Core (HasClient (.. ), RequestBody (RequestBodySource ), setRequestBody )
60
- import Servant.Docs
61
- import Servant.Foreign
63
+ import Servant.Docs hiding ( samples )
64
+ import Servant.Foreign hiding ( contentType )
62
65
import Servant.Server.Internal
63
66
import Servant.Types.SourceT (SourceT (.. ), source , StepT (.. ), fromActionStep )
64
67
import System.Directory
@@ -150,7 +153,10 @@ import qualified Data.ByteString.Lazy as LBS
150
153
-- after your handler has run, if they are still there. It is
151
154
-- therefore recommended to move or copy them somewhere in your
152
155
-- handler code if you need to keep the content around.
153
- data MultipartForm tag a
156
+ type MultipartForm tag a = MultipartForm' '[] tag a
157
+
158
+ -- | 'MultipartForm' which can be modified with 'Servant.API.Modifiers.Lenient'.
159
+ data MultipartForm' (mods :: [* ]) tag a
154
160
155
161
-- | What servant gets out of a @multipart/form-data@ form submission.
156
162
--
@@ -209,8 +215,11 @@ deriving instance Eq (MultipartResult tag) => Eq (FileData tag)
209
215
deriving instance Show (MultipartResult tag ) => Show (FileData tag )
210
216
211
217
-- | Lookup a file input with the given @name@ attribute.
212
- lookupFile :: Text -> MultipartData tag -> Maybe (FileData tag )
213
- lookupFile iname = find ((== iname) . fdInputName) . files
218
+ lookupFile :: Text -> MultipartData tag -> Either String (FileData tag )
219
+ lookupFile iname =
220
+ maybe (Left $ " File " <> cs iname <> " not found" ) Right
221
+ . find ((== iname) . fdInputName)
222
+ . files
214
223
215
224
-- | Representation for a textual input (any @\<input\>@ type but @file@).
216
225
--
@@ -221,8 +230,11 @@ data Input = Input
221
230
} deriving (Eq , Show )
222
231
223
232
-- | Lookup a textual input with the given @name@ attribute.
224
- lookupInput :: Text -> MultipartData tag -> Maybe Text
225
- lookupInput iname = fmap iValue . find ((== iname) . iName) . inputs
233
+ lookupInput :: Text -> MultipartData tag -> Either String Text
234
+ lookupInput iname =
235
+ maybe (Left $ " Field " <> cs iname <> " not found" ) (Right . iValue)
236
+ . find ((== iname) . iName)
237
+ . inputs
226
238
227
239
-- | 'MultipartData' is the type representing
228
240
-- @multipart/form-data@ form inputs. Sometimes
@@ -246,10 +258,10 @@ class FromMultipart tag a where
246
258
-- in a list of textual inputs and another list for
247
259
-- files, try to extract a value of type @a@. When
248
260
-- extraction fails, servant errors out with status code 400.
249
- fromMultipart :: MultipartData tag -> Maybe a
261
+ fromMultipart :: MultipartData tag -> Either String a
250
262
251
263
instance FromMultipart tag (MultipartData tag ) where
252
- fromMultipart = Just
264
+ fromMultipart = Right
253
265
254
266
-- | Allows you to tell servant how to turn a more structured type
255
267
-- into a 'MultipartData', which is what is actually sent by the
@@ -281,11 +293,12 @@ instance ToMultipart tag (MultipartData tag) where
281
293
instance ( FromMultipart tag a
282
294
, MultipartBackend tag
283
295
, LookupContext config (MultipartOptions tag )
296
+ , SBoolI (FoldLenient mods )
284
297
, HasServer sublayout config )
285
- => HasServer (MultipartForm tag a :> sublayout ) config where
298
+ => HasServer (MultipartForm' mods tag a :> sublayout ) config where
286
299
287
- type ServerT (MultipartForm tag a :> sublayout ) m =
288
- a -> ServerT sublayout m
300
+ type ServerT (MultipartForm' mods tag a :> sublayout ) m =
301
+ If ( FoldLenient mods ) ( Either String a ) a -> ServerT sublayout m
289
302
290
303
#if MIN_VERSION_servant_server(0,12,0)
291
304
hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy sublayout ) pc nt . s
@@ -299,16 +312,16 @@ instance ( FromMultipart tag a
299
312
popts = Proxy :: Proxy (MultipartOptions tag )
300
313
multipartOpts = fromMaybe (defaultMultipartOptions pbak)
301
314
$ lookupContext popts config
302
- subserver' = addMultipartHandling pbak multipartOpts subserver
315
+ subserver' = addMultipartHandling @ tag @ a @ mods pbak multipartOpts subserver
303
316
304
317
-- | Upon seeing @MultipartForm a :> ...@ in an API type,
305
318
-- servant-client will take a parameter of type @(LBS.ByteString, a)@,
306
319
-- where the bytestring is the boundary to use (see 'genBoundary'), and
307
320
-- replace the request body with the contents of the form.
308
321
instance (ToMultipart tag a , HasClient m api , MultipartBackend tag )
309
- => HasClient m (MultipartForm tag a :> api ) where
322
+ => HasClient m (MultipartForm' mods tag a :> api ) where
310
323
311
- type Client m (MultipartForm tag a :> api ) =
324
+ type Client m (MultipartForm' mods tag a :> api ) =
312
325
(LBS. ByteString , a ) -> Client m api
313
326
314
327
clientWithRoute pm _ req (boundary, param) =
@@ -352,7 +365,7 @@ genBoundary = LBS.pack
352
365
353
366
-- | Given a bytestring for the boundary, turns a `MultipartData` into
354
367
-- a 'RequestBody'
355
- multipartToBody :: forall tag .
368
+ multipartToBody :: forall tag .
356
369
MultipartBackend tag
357
370
=> LBS. ByteString
358
371
-> MultipartData tag
@@ -373,7 +386,7 @@ multipartToBody boundary mp = RequestBodySource $ files' <> source ["--", bounda
373
386
mempty' = SourceT ($ Stop )
374
387
crlf = " \r\n "
375
388
lencode = LBS. fromStrict . encodeUtf8
376
- renderInput input = renderPart (lencode . iName $ input)
389
+ renderInput input = renderPart (lencode . iName $ input)
377
390
" text/plain"
378
391
" "
379
392
(source . pure . lencode . iValue $ input)
@@ -420,10 +433,11 @@ check pTag tag = withRequest $ \request -> do
420
433
where parseOpts = generalOptions tag
421
434
422
435
-- Add multipart extraction support to a Delayed.
423
- addMultipartHandling :: forall tag multipart env a . (FromMultipart tag multipart , MultipartBackend tag )
436
+ addMultipartHandling :: forall tag multipart (mods :: [* ]) env a . (FromMultipart tag multipart , MultipartBackend tag )
437
+ => SBoolI (FoldLenient mods )
424
438
=> Proxy tag
425
439
-> MultipartOptions tag
426
- -> Delayed env (multipart -> a )
440
+ -> Delayed env (If ( FoldLenient mods ) ( Either String multipart ) multipart -> a )
427
441
-> Delayed env a
428
442
addMultipartHandling pTag opts subserver =
429
443
addBodyCheck subserver contentCheck bodyCheck
@@ -433,10 +447,11 @@ addMultipartHandling pTag opts subserver =
433
447
434
448
bodyCheck () = do
435
449
mpd <- check pTag opts :: DelayedIO (MultipartData tag )
436
- case fromMultipart mpd of
437
- Nothing -> liftRouteResult $ FailFatal
438
- err400 { errBody = " fromMultipart returned Nothing" }
439
- Just x -> return x
450
+ case (sbool :: SBool (FoldLenient mods ), fromMultipart @ tag @ multipart mpd ) of
451
+ (SFalse , Left msg) -> liftRouteResult $ FailFatal
452
+ err400 { errBody = " Could not decode multipart mime body: " <> cs msg }
453
+ (SFalse , Right x) -> return x
454
+ (STrue , res) -> return $ either (Left . cs) Right res
440
455
441
456
contentTypeH req = fromMaybe " application/octet-stream" $
442
457
lookup " Content-Type" (requestHeaders req)
@@ -502,7 +517,7 @@ instance MultipartBackend Tmp where
502
517
type MultipartBackendOptions Tmp = TmpBackendOptions
503
518
504
519
defaultBackendOptions _ = defaultTmpBackendOptions
505
- -- streams the file from disk
520
+ -- streams the file from disk
506
521
loadFile _ fp =
507
522
SourceT $ \ k ->
508
523
withFile fp ReadMode $ \ hdl ->
@@ -519,7 +534,7 @@ instance MultipartBackend Mem where
519
534
520
535
defaultBackendOptions _ = ()
521
536
loadFile _ = source . pure
522
- backend _ opts _ = lbsBackEnd
537
+ backend _ _ _ = lbsBackEnd
523
538
524
539
-- | Configuration for the temporary file based backend.
525
540
--
@@ -563,8 +578,8 @@ instance LookupContext '[] a where
563
578
564
579
instance {-# OVERLAPPABLE #-}
565
580
LookupContext cs a => LookupContext (c ': cs ) a where
566
- lookupContext p (c :. cs ) =
567
- lookupContext p cs
581
+ lookupContext p (_ :. cxts ) =
582
+ lookupContext p cxts
568
583
569
584
instance {-# OVERLAPPING #-}
570
585
LookupContext cs a => LookupContext (a ': cs ) a where
0 commit comments