-
-
Notifications
You must be signed in to change notification settings - Fork 412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
File upload combinator #133
Comments
This is great! 👍 |
One problem with this is that we can't target individual files by specifying the input name associated to them, with, say, a
This is why I'm not sure we should include a |
Here's a version that doesn't "forget" about the inputs that were sent along with the files in the request body (remember, this is {-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Files where
import Control.Monad.IO.Class
import Control.Monad.Trans.Either
import Control.Monad.Trans.Resource
import Data.ByteString.Lazy (ByteString)
import Network.Wai
import Network.Wai.Handler.Warp (run)
import Network.Wai.Parse
import Servant
import Servant.Server.Internal
-- Backends for file upload: in memory or in /tmp ?
data Mem
data Tmp
class KnownBackend b where
type Storage b :: *
withBackend :: Proxy b -> (BackEnd (Storage b) -> IO r) -> IO r
instance KnownBackend Mem where
type Storage Mem = ByteString
withBackend Proxy f = f lbsBackEnd
instance KnownBackend Tmp where
type Storage Tmp = FilePath
withBackend Proxy f = runResourceT . withInternalState $ \s ->
f (tempFileBackEnd s)
-- * Files combinator, to get all of the uploaded files
data Files b
type MultiPartData b = ([Param], [File (Storage b)])
instance (KnownBackend b, HasServer api) => HasServer (Files b :> api) where
type ServerT (Files b :> api) m =
MultiPartData b -> ServerT api m
route Proxy subserver req respond = withBackend pb $ \b -> do
dat <- parseRequestBody b req
route (Proxy :: Proxy api) (subserver dat) req respond
where pb = Proxy :: Proxy b
type FilesMem = Files Mem
type FilesTmp = Files Tmp
-- test
type API = "files" :> FilesTmp :> Post '[JSON] ()
:<|> Raw
api :: Proxy API
api = Proxy
server :: Server API
server = filesHandler :<|> serveDirectory "."
where filesHandler :: MultiPartData Tmp -> EitherT ServantErr IO ()
filesHandler (inputs, files) = do
liftIO $ mapM_ ppFile files
liftIO $ mapM_ print inputs
ppFile :: File FilePath -> IO ()
ppFile (name, fileinfo) = do
putStrLn $ "Input name: " ++ show name
putStrLn $ "File name: " ++ show (fileName fileinfo)
putStrLn $ "Content type: " ++ show (fileContentType fileinfo)
putStrLn $ "------- Content --------"
readFile (fileContent fileinfo) >>= putStrLn
putStrLn $ "------------------------"
app :: Application
app = serve api server Now, this is in fact all equivalent to a new content type, for Thoughts? @jkarni any idea to make this a content type? |
fwiw, i'm using this now and while it works from curl, it doesn't in the browser for large files. Weirdly, if i use netcat to collect the request as raw text then use netcat to dump it into the server, it works fine - doing it normally gets 405 Method Not Allowed and some chunks of text from the input logged by Network.Wai.Middleware.RequestLogger. |
Interesting. I can upload files just fine in some app I'm working on. Do you think you could share a minimal app (I assume the one you're working on is for work, hence closed-source?) that has this problem? I could start from there and investigate what is going on. So just the HTML of your upload form along with the haskell app that receives the files. |
yep, i'll try to get that to you today. |
i think this is actually a warp thing, i get it when i use scotty too. |
@mwotton what version of warp? |
3.1.0. testing with the latest now. |
@mwotton there was an issue with 3.1.0. So try the lastest or downgrade to 3.0.*. |
ah, thanks, @codedmart - i'll try that. |
yep, 3.1.2 fixes it. wish i'd known that before i rewrote the app :) |
Ah, good to know! |
Perhaps you could release this as a package? |
Well, we would have to write instances for servant-client, servant-docs etc if we want to release this properly. It's quite a task... |
Make the version < 1. Something is better than nothing, IMHO. That is to say, this is awesome and I would like to use it. |
In order to make this a content-type, we'd need to change the content-type machinery to allow IO. I think that makes sense. And if we do that, we may not need instances for all the packages. |
@3noch You can use this! Simply drop the code in a module in your project, and you can use it =) I've put this combinator to work in several apps for work this way. |
@alpmestan Of course, I just wish I had found it on hackage instead of a GitHub issue. Also, I could submit a PR to a repo if it existed. 😀 |
Yeah we should probably put some page together with combinators like this one and some instances. On the github wiki or in the |
👍 |
Is this still the best way to achieve file upload? |
I have it working. I am trying to capture the text and upload at the same time. How do I do that: |
@i-am-the-slime Did you look at this version: #133 (comment). It handles the form data and the files. |
Thanks @codedmart it works. However for a test I now seem to need a |
Is there a version of this that works with 0.5? |
@schell: Yes, that's correct. |
I tried to update the combinator above for Servant 0.8. As far as I can tell, the change is not that simple as the route method in the HasServer class does not have access to the request object anymore. Any suggestion as to how I might proceed ? |
Nevermind, looking at Servant.Server.Internal gave me the answer. I still hope this combinator will be officially included in Servant, though ! |
Well, it's "just" a matter of writing all interpretations for it =) The annoying thing is that it basically just is a content type, but one that needs IO to decode from, and it doesn't feel right. Those are the 2 main reasons this isn't shipped in servant today. |
There's no way around IO without avoiding |
right. and we do want some IO because files get created using that functionand the /tmp backend. |
I think this is the wrong approach. If we only allow new combinators to pop up when they provide instances for all core interpretations, we're basically asking people to write code that they don't even want to use themselves. Which we should strongly avoid. @gaeldeest (or anyone else): Have you considered publishing this combinator as a separate package? Then you could just include the one ( Re: Allowing combinators to do IO: Even if most combinators don't want (and shouldn't) use |
+1, please publish this as a package or get it merged into servant and worry about the other instances later. Pretty please! |
Having played around with @fizruk 's branch, I now think allowing
So I'm jumping ships and saying that the approach @alpmestan outlined is better. |
@jkarni I am not sure which of @alpmestan's approaches you refer to. What are the alternatives to IO-enabled |
The alternative is to just have a |
Multipart needs to be supported though. |
Right, I didn't mean that we shouldn't support multipart at some point. But I think people really just want the code shown in this ticket to be available on hackage and ready to use in any servant app, for now. Later on, when someone feels brave enough, we could have proper multipart support but this means reimplementing the code from wai-extra, even though there's still the question then of how to do it without |
@alpmestan As long as I have the ability to still handle mutlipart myself then I am good with whatever. |
Will the solution discussed here support the
|
Hi, I've been playing with @alpmestan's approach for handling file uploads using the suggested {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RecordWildCards #-}
module Main where
import Control.Monad.Trans.Resource
import Control.Monad.Except
import Data.Monoid
import qualified Data.ByteString.Lazy as LS
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Lucid
import Network.Wai.Parse
import Network.Wai.Handler.Warp hiding (FileInfo)
import Servant
import Servant.HTML.Lucid
import Servant.Server.Internal
import System.Directory
import System.FilePath
data Mem
data Tmp
class KnownBackend b where
type Storage b :: * -- associated type family
withBackend :: Proxy b -> (BackEnd (Storage b) -> IO r) -> IO r
instance KnownBackend Mem where
type Storage Mem = LS.ByteString
withBackend Proxy f = f lbsBackEnd
instance KnownBackend Tmp where
type Storage Tmp = FilePath
withBackend Proxy f = runResourceT . withInternalState $ \s ->
f (tempFileBackEnd s)
data Files b
type MultiPartData b = ([Param], [File (Storage b)])
instance (KnownBackend b, HasServer sublayout config)
=> HasServer (Files b :> sublayout) config where
type ServerT (Files b :> sublayout) m =
IO (MultiPartData b) -> ServerT sublayout m
route Proxy config subserver =
route psub config (addBodyCheck subserver check)
where
psub = Proxy :: Proxy sublayout
pbak = Proxy :: Proxy b
check = withRequest $ \request -> return $ withBackend pbak $
\backend -> parseRequestBody backend request
type API = "form" :> Get '[HTML] (Html ())
:<|> "upload" :> Files Mem :> Post '[PlainText] Text
-- | Some form to upload an image and a video file.
formHandler :: ExceptT ServantErr IO (Html ())
formHandler = return $
html_ $ do
head_ (title_ "upload files test!")
body_ $ do
h1_ "upload files test"
form_ [ action_ "/upload"
, method_ "POST"
, enctype_ "multipart/form-data" ] $ do
input_ [ type_ "file", name_ "media" ]
input_ [ type_ "submit"
, name_ "send"
, value_ "Submit Data!" ]
-- | Handle the uploaded image and video files.
uploadHandler :: IO (MultiPartData Mem) -> ExceptT ServantErr IO Text
uploadHandler multipart = do
liftIO $ putStrLn "handling file upload..."
(params, files) <- liftIO multipart
-- when using `MultiPartData Mem`, use `wrFile` to write the files from memory to disk.
-- when using `MultiPartData Tmp`, use `cpFile` to copy the temporarily uploaded files
-- to some other location.
liftIO $ mapM_ wrFile files
return $
"params:\n" <> Text.intercalate "\n" (map ppParam params) <> "\n" <>
"files:\n" <> Text.intercalate "\n" (map ppFile files)
where
ppParam (name, val) =
" name = " <> Text.decodeUtf8 name <> "\n" <>
" value = " <> Text.decodeUtf8 val <> "\n"
ppFile (paramName, FileInfo{..}) =
" parameter name = " <> Text.decodeUtf8 paramName <> "\n" <>
" fileName = " <> Text.decodeUtf8 fileName <> "\n" <>
" fileContentType = " <> Text.decodeUtf8 fileContentType <> "\n" <>
" fileContent = " <> "..." <> "\n"
wrFile (_, FileInfo{..}) = LS.writeFile newFileName fileContent
where newFileName = (Text.unpack . Text.decodeUtf8) fileName <.> "copy"
cpFile (_, FileInfo{..}) = copyFile oldFileName newFileName
where
oldFileName = fileContent
newFileName = (Text.unpack . Text.decodeUtf8) fileName <.> "copy"
server :: Server API
server = formHandler :<|> uploadHandler
main :: IO ()
main = run 8888 (serve (Proxy :: Proxy API) server) However, the above snippet exhibits the following two issues:
Has anyone observed either one of these issues? And if so, does anyone know what is happening here and, at best, how to resolve and overcome any of the issues? Thoughts on this would be great! |
In my first shot at file upload,
In your case, AFAICT, we're only guaranteed that the tmp file is there during the decoding and writing to /tmp. I really wish we had a solid multipart encoding/decoding library; this would allow:
There's a possibility that I am misunderstanding the issue, but that looks like the most plausible explanation to me. |
@alpmestan @bollmann The temp files are deleted at the end of |
@alpmestan, @rimmington: Thanks for your replies! Indeed, using the |
For the record, I've been working on packaging up the multipart/form-data-powered upload. It does a little bit more than all the code we've written in this ticket and doesn't have the issue reported by @bollmann, without exposing a continuation. I still have a few things to add there and have to think about making this as nice and simple to use as possible, but I'll drop a comment here once it's ready. The repo's here. Hopefully we'll soon be able to close one of (if not the) oldest open issues in the tracker =) |
Any updates on this feature? I'd really like to have file uploads in servant! |
Well, I'd appreciate feedback on https://github.com/haskell-servant/servant-multipart if anyone has got some time for looking at it. I might get back to it soon and add support for in-memory handling of file upload and cut a first release. It requires these patches for servant, which I yet have to wrap up and add tests for. If anyone wants to give some feedback or even help with these tasks, that'd be very much appreciated :) |
For the record, servant got the necessary patches and https://github.com/haskell-servant/servant-multipart should be ready for use. It's missing support for in-memory uploads but could be released as it stands. |
It looks good! I'd say release it! Though maybe it should be split into As for the feedback - it's well documented and the API seems very nice! I'd mention something about Primarily, though, release it! |
Just creating this issue to put some code I have written up here for discussion.
along with this HTML file:
served through
serveDirectory
. Thoughts, comments?The text was updated successfully, but these errors were encountered: