8
8
{-# LANGUAGE ScopedTypeVariables #-}
9
9
{-# LANGUAGE TypeSynonymInstances #-}
10
10
{-# LANGUAGE MultiParamTypeClasses #-}
11
+ {-# LANGUAGE StandaloneDeriving #-}
12
+ {-# LANGUAGE UndecidableInstances #-}
11
13
-- | @multipart/form-data@ support for servant.
12
14
--
13
15
-- This is mostly useful for adding file upload support to
@@ -21,6 +23,7 @@ module Servant.Multipart
21
23
, MultipartOptions (.. )
22
24
, defaultMultipartOptions
23
25
, TmpBackendOptions (.. )
26
+ , LbsBackendOptions (.. )
24
27
, defaultTmpBackendOptions
25
28
, Input (.. )
26
29
, FileData (.. )
@@ -125,7 +128,7 @@ import qualified Data.ByteString.Lazy as LBS
125
128
-- after your handler has run, if they are still there. It is
126
129
-- therefore recommended to move or copy them somewhere in your
127
130
-- handler code if you need to keep the content around.
128
- data MultipartForm a
131
+ data MultipartForm options a
129
132
130
133
-- | What servant gets out of a @multipart/form-data@ form submission.
131
134
--
@@ -139,20 +142,22 @@ data MultipartForm a
139
142
-- 'FileData' which among other things contains the path to the temporary file
140
143
-- (to be removed when your handler is done running) with a given uploaded
141
144
-- file's content. See haddocks for 'FileData'.
142
- data MultipartData = MultipartData
145
+ data MultipartData options = MultipartData
143
146
{ inputs :: [Input ]
144
- , files :: [FileData ]
147
+ , files :: [FileData options ]
145
148
}
146
149
150
+
151
+
147
152
-- TODO: this is specific to Tmp. we need a version that
148
153
-- can handle Mem as well.
149
- fromRaw :: ([Network.Wai.Parse. Param ], [File FilePath ]) -> MultipartData
154
+ fromRaw :: forall options . ([Network.Wai.Parse. Param ], [File ( MultipartResult options ) ]) -> MultipartData options
150
155
fromRaw (inputs, files) = MultipartData is fs
151
156
152
157
where is = map (\ (name, val) -> Input (dec name) (dec val)) inputs
153
158
fs = map toFile files
154
159
155
- toFile :: File FilePath -> FileData
160
+ toFile :: File ( MultipartResult options ) -> FileData options
156
161
toFile (iname, fileinfo) =
157
162
FileData (dec iname)
158
163
(dec $ fileName fileinfo)
@@ -164,20 +169,24 @@ fromRaw (inputs, files) = MultipartData is fs
164
169
-- | Representation for an uploaded file, usually resulting from
165
170
-- picking a local file for an HTML input that looks like
166
171
-- @\<input type="file" name="somefile" /\>@.
167
- data FileData = FileData
172
+ data FileData options = FileData
168
173
{ fdInputName :: Text -- ^ @name@ attribute of the corresponding
169
174
-- HTML @\<input\>@
170
175
, fdFileName :: Text -- ^ name of the file on the client's disk
171
176
, fdFileCType :: Text -- ^ MIME type for the file
172
- , fdFilePath :: FilePath -- ^ path to the temporary file that has the
177
+ , fdPayload :: MultipartResult options
178
+ -- ^ path to the temporary file that has the
173
179
-- content of the user's original file. Only
174
180
-- valid during the execution of your handler as
175
181
-- it gets removed right after, which means you
176
182
-- really want to move or copy it in your handler.
177
- } deriving (Eq , Show )
183
+ }
184
+
185
+ deriving instance Eq (MultipartResult options ) => Eq (FileData options )
186
+ deriving instance Show (MultipartResult options ) => Show (FileData options )
178
187
179
188
-- | Lookup a file input with the given @name@ attribute.
180
- lookupFile :: Text -> MultipartData -> Maybe FileData
189
+ lookupFile :: Text -> MultipartData options -> Maybe ( FileData options )
181
190
lookupFile iname = find ((== iname) . fdInputName) . files
182
191
183
192
-- | Representation for a textual input (any @\<input\>@ type but @file@).
@@ -189,7 +198,7 @@ data Input = Input
189
198
} deriving (Eq , Show )
190
199
191
200
-- | Lookup a textual input with the given @name@ attribute.
192
- lookupInput :: Text -> MultipartData -> Maybe Text
201
+ lookupInput :: Text -> MultipartData options -> Maybe Text
193
202
lookupInput iname = fmap iValue . find ((== iname) . iName) . inputs
194
203
195
204
-- | 'MultipartData' is the type representing
@@ -209,34 +218,35 @@ lookupInput iname = fmap iValue . find ((==iname) . iName) . inputs
209
218
-- User \<$\> lookupInput "username" (inputs form)
210
219
-- \<*\> fmap fdFilePath (lookupFile "pic" $ files form)
211
220
-- @
212
- class FromMultipart a where
221
+ class FromMultipart options a where
213
222
-- | Given a value of type 'MultipartData', which consists
214
223
-- in a list of textual inputs and another list for
215
224
-- files, try to extract a value of type @a@. When
216
225
-- extraction fails, servant errors out with status code 400.
217
- fromMultipart :: MultipartData -> Maybe a
226
+ fromMultipart :: MultipartData options -> Maybe a
218
227
219
- instance FromMultipart MultipartData where
228
+ instance FromMultipart options ( MultipartData options ) where
220
229
fromMultipart = Just
221
230
222
231
-- | Upon seeing @MultipartForm a :> ...@ in an API type,
223
232
--- servant-server will hand a value of type @a@ to your handler
224
233
-- assuming the request body's content type is
225
234
-- @multipart/form-data@ and the call to 'fromMultipart' succeeds.
226
- instance ( FromMultipart a
227
- , LookupContext config MultipartOptions
235
+ instance ( FromMultipart options a
236
+ , MultipartBackend options
237
+ , LookupContext config (MultipartOptions options )
228
238
, HasServer sublayout config )
229
- => HasServer (MultipartForm a :> sublayout ) config where
239
+ => HasServer (MultipartForm options a :> sublayout ) config where
230
240
231
- type ServerT (MultipartForm a :> sublayout ) m =
241
+ type ServerT (MultipartForm options a :> sublayout ) m =
232
242
a -> ServerT sublayout m
233
243
234
244
route Proxy config subserver =
235
245
route psub config subserver'
236
246
where
237
247
psub = Proxy :: Proxy sublayout
238
248
pbak = Proxy :: Proxy b
239
- popts = Proxy :: Proxy MultipartOptions
249
+ popts = Proxy :: Proxy ( MultipartOptions options )
240
250
multipartOpts = fromMaybe defaultMultipartOptions
241
251
$ lookupContext popts config
242
252
subserver' = addMultipartHandling multipartOpts subserver
@@ -245,16 +255,16 @@ instance ( FromMultipart a
245
255
-- returning the data as well as the resourcet InternalState
246
256
-- that allows us to properly clean up the temporary files
247
257
-- later on.
248
- check :: MultipartOptions -> DelayedIO MultipartData
258
+ check :: MultipartBackend options => MultipartOptions options -> DelayedIO ( MultipartData options )
249
259
check opts = withRequest $ \ request -> do
250
260
st <- liftResourceT getInternalState
251
- rawData <- liftIO $ parseRequestBodyEx parseOpts (tmpBackend opts st) request
261
+ rawData <- liftIO $ parseRequestBodyEx parseOpts (backend (options opts) st) request
252
262
return (fromRaw rawData)
253
263
where parseOpts = generalOptions opts
254
264
255
265
-- Add multipart extraction support to a Delayed.
256
- addMultipartHandling :: FromMultipart multipart
257
- => MultipartOptions
266
+ addMultipartHandling :: forall options multipart env a . ( FromMultipart options multipart , MultipartBackend options )
267
+ => MultipartOptions options
258
268
-> Delayed env (multipart -> a )
259
269
-> Delayed env a
260
270
addMultipartHandling opts subserver =
@@ -264,7 +274,7 @@ addMultipartHandling opts subserver =
264
274
fuzzyMultipartCTCheck (contentTypeH request)
265
275
266
276
bodyCheck () = do
267
- mpd <- check opts :: DelayedIO MultipartData
277
+ mpd <- check opts :: DelayedIO ( MultipartData options )
268
278
case fromMultipart mpd of
269
279
Nothing -> liftRouteResult $ FailFatal
270
280
err400 { errBody = " fromMultipart returned Nothing" }
@@ -289,17 +299,6 @@ fuzzyMultipartCTCheck ct
289
299
" multipart/form-data" | Just _bound <- lookup " boundary" attrs -> True
290
300
_ -> False
291
301
292
- tmpBackend :: MultipartOptions
293
- -> InternalState
294
- -> ignored1
295
- -> ignored2
296
- -> IO SBS. ByteString
297
- -> IO FilePath
298
- tmpBackend opts =
299
- tempFileBackEndOpts (getTmpDir tmpOpts) (filenamePat tmpOpts)
300
- where
301
- tmpOpts = tmpOptions opts
302
-
303
302
-- | Global options for configuring how the
304
303
-- server should handle multipart data.
305
304
--
@@ -309,11 +308,40 @@ tmpBackend opts =
309
308
-- the temporary file backend. See haddocks for
310
309
-- 'ParseRequestBodyOptions' and 'TmpBackendOptions' respectively
311
310
-- for more information on what you can tweak.
312
- data MultipartOptions = MultipartOptions
311
+ data MultipartOptions options = MultipartOptions
313
312
{ generalOptions :: ParseRequestBodyOptions
314
- , tmpOptions :: TmpBackendOptions
313
+ , options :: options
315
314
}
316
315
316
+ class MultipartBackend options where
317
+ type MultipartResult options :: *
318
+
319
+ backend :: options
320
+ -> InternalState
321
+ -> ignored1
322
+ -> ignored2
323
+ -> IO SBS. ByteString
324
+ -> IO (MultipartResult options )
325
+
326
+ defaultBackendOptions :: options
327
+
328
+ instance MultipartBackend TmpBackendOptions where
329
+ type MultipartResult TmpBackendOptions = FilePath
330
+
331
+ defaultBackendOptions = defaultTmpBackendOptions
332
+ backend opts = tmpBackend
333
+ where
334
+ tmpBackend = tempFileBackEndOpts (getTmpDir opts) (filenamePat opts)
335
+
336
+ instance MultipartBackend LbsBackendOptions where
337
+ type MultipartResult LbsBackendOptions = LBS. ByteString
338
+
339
+ defaultBackendOptions = LbsBackendOptions
340
+ backend opts _ = lbsBackEnd
341
+
342
+ -- | TODO: write me
343
+ data LbsBackendOptions = LbsBackendOptions
344
+
317
345
-- | Configuration for the temporary file based backend.
318
346
--
319
347
-- You can configure the way servant-multipart gets its hands
@@ -338,10 +366,10 @@ defaultTmpBackendOptions = TmpBackendOptions
338
366
--
339
367
-- Uses 'defaultParseRequestBodyOptions' and
340
368
-- 'defaultTmpBackendOptions' respectively.
341
- defaultMultipartOptions :: MultipartOptions
369
+ defaultMultipartOptions :: MultipartBackend options => MultipartOptions options
342
370
defaultMultipartOptions = MultipartOptions
343
371
{ generalOptions = defaultParseRequestBodyOptions
344
- , tmpOptions = defaultTmpBackendOptions
372
+ , options = defaultBackendOptions
345
373
}
346
374
347
375
-- Utility class that's like HasContextEntry
0 commit comments