Skip to content

Commit 34b69a6

Browse files
author
Matti Räty
committed
WIP: upload files to memory
1 parent 14c3d74 commit 34b69a6

File tree

3 files changed

+72
-42
lines changed

3 files changed

+72
-42
lines changed

exe/Upload.hs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import Network.Wai.Handler.Warp
1414
import Servant
1515
import Servant.Multipart
1616

17+
import qualified Data.ByteString.Lazy as LBS
18+
1719
-- Our API, which consists in a single POST endpoint at /
1820
-- that takes a multipart/form-data request body and
1921
-- pretty-prints the data it got to stdout before returning 0.
20-
type API = MultipartForm MultipartData :> Post '[JSON] Integer
22+
type API = MultipartForm LbsBackendOptions (MultipartData LbsBackendOptions) :> Post '[JSON] Integer
2123

2224
api :: Proxy API
2325
api = Proxy
@@ -38,10 +40,9 @@ upload multipartData = do
3840
++ " -> " ++ show (iValue input)
3941

4042
forM_ (files multipartData) $ \file -> do
41-
content <- readFile (fdFilePath file)
43+
let content = fdFilePath file
4244
putStrLn $ "Content of " ++ show (fdFileName file)
43-
++ " at " ++ fdFilePath file
44-
putStrLn content
45+
LBS.putStr content
4546
return 0
4647

4748
startServer :: IO ()

servant-multipart.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ executable upload
4242
build-depends:
4343
base,
4444
http-client,
45+
bytestring,
4546
network,
4647
servant,
4748
servant-multipart,

src/Servant/Multipart.hs

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
{-# LANGUAGE ScopedTypeVariables #-}
99
{-# LANGUAGE TypeSynonymInstances #-}
1010
{-# LANGUAGE MultiParamTypeClasses #-}
11+
{-# LANGUAGE StandaloneDeriving #-}
12+
{-# LANGUAGE UndecidableInstances #-}
1113
-- | @multipart/form-data@ support for servant.
1214
--
1315
-- This is mostly useful for adding file upload support to
@@ -21,6 +23,7 @@ module Servant.Multipart
2123
, MultipartOptions(..)
2224
, defaultMultipartOptions
2325
, TmpBackendOptions(..)
26+
, LbsBackendOptions(..)
2427
, defaultTmpBackendOptions
2528
, Input(..)
2629
, FileData(..)
@@ -125,7 +128,7 @@ import qualified Data.ByteString.Lazy as LBS
125128
-- after your handler has run, if they are still there. It is
126129
-- therefore recommended to move or copy them somewhere in your
127130
-- handler code if you need to keep the content around.
128-
data MultipartForm a
131+
data MultipartForm options a
129132

130133
-- | What servant gets out of a @multipart/form-data@ form submission.
131134
--
@@ -139,20 +142,22 @@ data MultipartForm a
139142
-- 'FileData' which among other things contains the path to the temporary file
140143
-- (to be removed when your handler is done running) with a given uploaded
141144
-- file's content. See haddocks for 'FileData'.
142-
data MultipartData = MultipartData
145+
data MultipartData options = MultipartData
143146
{ inputs :: [Input]
144-
, files :: [FileData]
147+
, files :: [FileData options]
145148
}
146149

150+
151+
147152
-- TODO: this is specific to Tmp. we need a version that
148153
-- 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
150155
fromRaw (inputs, files) = MultipartData is fs
151156

152157
where is = map (\(name, val) -> Input (dec name) (dec val)) inputs
153158
fs = map toFile files
154159

155-
toFile :: File FilePath -> FileData
160+
toFile :: File (MultipartResult options) -> FileData options
156161
toFile (iname, fileinfo) =
157162
FileData (dec iname)
158163
(dec $ fileName fileinfo)
@@ -164,20 +169,24 @@ fromRaw (inputs, files) = MultipartData is fs
164169
-- | Representation for an uploaded file, usually resulting from
165170
-- picking a local file for an HTML input that looks like
166171
-- @\<input type="file" name="somefile" /\>@.
167-
data FileData = FileData
172+
data FileData options = FileData
168173
{ fdInputName :: Text -- ^ @name@ attribute of the corresponding
169174
-- HTML @\<input\>@
170175
, fdFileName :: Text -- ^ name of the file on the client's disk
171176
, 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
173179
-- content of the user's original file. Only
174180
-- valid during the execution of your handler as
175181
-- it gets removed right after, which means you
176182
-- 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)
178187

179188
-- | Lookup a file input with the given @name@ attribute.
180-
lookupFile :: Text -> MultipartData -> Maybe FileData
189+
lookupFile :: Text -> MultipartData options -> Maybe (FileData options)
181190
lookupFile iname = find ((==iname) . fdInputName) . files
182191

183192
-- | Representation for a textual input (any @\<input\>@ type but @file@).
@@ -189,7 +198,7 @@ data Input = Input
189198
} deriving (Eq, Show)
190199

191200
-- | Lookup a textual input with the given @name@ attribute.
192-
lookupInput :: Text -> MultipartData -> Maybe Text
201+
lookupInput :: Text -> MultipartData options -> Maybe Text
193202
lookupInput iname = fmap iValue . find ((==iname) . iName) . inputs
194203

195204
-- | 'MultipartData' is the type representing
@@ -209,34 +218,35 @@ lookupInput iname = fmap iValue . find ((==iname) . iName) . inputs
209218
-- User \<$\> lookupInput "username" (inputs form)
210219
-- \<*\> fmap fdFilePath (lookupFile "pic" $ files form)
211220
-- @
212-
class FromMultipart a where
221+
class FromMultipart options a where
213222
-- | Given a value of type 'MultipartData', which consists
214223
-- in a list of textual inputs and another list for
215224
-- files, try to extract a value of type @a@. When
216225
-- extraction fails, servant errors out with status code 400.
217-
fromMultipart :: MultipartData -> Maybe a
226+
fromMultipart :: MultipartData options -> Maybe a
218227

219-
instance FromMultipart MultipartData where
228+
instance FromMultipart options (MultipartData options) where
220229
fromMultipart = Just
221230

222231
-- | Upon seeing @MultipartForm a :> ...@ in an API type,
223232
--- servant-server will hand a value of type @a@ to your handler
224233
-- assuming the request body's content type is
225234
-- @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)
228238
, HasServer sublayout config )
229-
=> HasServer (MultipartForm a :> sublayout) config where
239+
=> HasServer (MultipartForm options a :> sublayout) config where
230240

231-
type ServerT (MultipartForm a :> sublayout) m =
241+
type ServerT (MultipartForm options a :> sublayout) m =
232242
a -> ServerT sublayout m
233243

234244
route Proxy config subserver =
235245
route psub config subserver'
236246
where
237247
psub = Proxy :: Proxy sublayout
238248
pbak = Proxy :: Proxy b
239-
popts = Proxy :: Proxy MultipartOptions
249+
popts = Proxy :: Proxy (MultipartOptions options)
240250
multipartOpts = fromMaybe defaultMultipartOptions
241251
$ lookupContext popts config
242252
subserver' = addMultipartHandling multipartOpts subserver
@@ -245,16 +255,16 @@ instance ( FromMultipart a
245255
-- returning the data as well as the resourcet InternalState
246256
-- that allows us to properly clean up the temporary files
247257
-- later on.
248-
check :: MultipartOptions -> DelayedIO MultipartData
258+
check :: MultipartBackend options => MultipartOptions options -> DelayedIO (MultipartData options)
249259
check opts = withRequest $ \request -> do
250260
st <- liftResourceT getInternalState
251-
rawData <- liftIO $ parseRequestBodyEx parseOpts (tmpBackend opts st) request
261+
rawData <- liftIO $ parseRequestBodyEx parseOpts (backend (options opts) st) request
252262
return (fromRaw rawData)
253263
where parseOpts = generalOptions opts
254264

255265
-- 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
258268
-> Delayed env (multipart -> a)
259269
-> Delayed env a
260270
addMultipartHandling opts subserver =
@@ -264,7 +274,7 @@ addMultipartHandling opts subserver =
264274
fuzzyMultipartCTCheck (contentTypeH request)
265275

266276
bodyCheck () = do
267-
mpd <- check opts :: DelayedIO MultipartData
277+
mpd <- check opts :: DelayedIO (MultipartData options)
268278
case fromMultipart mpd of
269279
Nothing -> liftRouteResult $ FailFatal
270280
err400 { errBody = "fromMultipart returned Nothing" }
@@ -289,17 +299,6 @@ fuzzyMultipartCTCheck ct
289299
"multipart/form-data" | Just _bound <- lookup "boundary" attrs -> True
290300
_ -> False
291301

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-
303302
-- | Global options for configuring how the
304303
-- server should handle multipart data.
305304
--
@@ -309,11 +308,40 @@ tmpBackend opts =
309308
-- the temporary file backend. See haddocks for
310309
-- 'ParseRequestBodyOptions' and 'TmpBackendOptions' respectively
311310
-- for more information on what you can tweak.
312-
data MultipartOptions = MultipartOptions
311+
data MultipartOptions options = MultipartOptions
313312
{ generalOptions :: ParseRequestBodyOptions
314-
, tmpOptions :: TmpBackendOptions
313+
, options :: options
315314
}
316315

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+
317345
-- | Configuration for the temporary file based backend.
318346
--
319347
-- You can configure the way servant-multipart gets its hands
@@ -338,10 +366,10 @@ defaultTmpBackendOptions = TmpBackendOptions
338366
--
339367
-- Uses 'defaultParseRequestBodyOptions' and
340368
-- 'defaultTmpBackendOptions' respectively.
341-
defaultMultipartOptions :: MultipartOptions
369+
defaultMultipartOptions :: MultipartBackend options => MultipartOptions options
342370
defaultMultipartOptions = MultipartOptions
343371
{ generalOptions = defaultParseRequestBodyOptions
344-
, tmpOptions = defaultTmpBackendOptions
372+
, options = defaultBackendOptions
345373
}
346374

347375
-- Utility class that's like HasContextEntry

0 commit comments

Comments
 (0)