{"action":"created","issue":{"url":"https://api.github.com/repos/haskell-servant/servant/issues/133","labels_url":"https://api.github.com/repos/haskell-servant/servant/issues/133/labels{/name}","comments_url":"https://api.github.com/repos/haskell-servant/servant/issues/133/comments","events_url":"https://api.github.com/repos/haskell-servant/servant/issues/133/events","html_url":"https://github.com/haskell-servant/servant/issues/133","id":89623794,"number":133,"title":"File upload combinator","user":{"login":"alpmestan","id":119323,"avatar_url":"https://avatars.githubusercontent.com/u/119323?v=3","gravatar_id":"","url":"https://api.github.com/users/alpmestan","html_url":"https://github.com/alpmestan","followers_url":"https://api.github.com/users/alpmestan/followers","following_url":"https://api.github.com/users/alpmestan/following{/other_user}","gists_url":"https://api.github.com/users/alpmestan/gists{/gist_id}","starred_url":"https://api.github.com/users/alpmestan/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/alpmestan/subscriptions","organizations_url":"https://api.github.com/users/alpmestan/orgs","repos_url":"https://api.github.com/users/alpmestan/repos","events_url":"https://api.github.com/users/alpmestan/events{/privacy}","received_events_url":"https://api.github.com/users/alpmestan/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/haskell-servant/servant/labels/enhancement","name":"enhancement","color":"84b6eb"}],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":7,"created_at":"2015-06-19T18:01:07Z","updated_at":"2015-08-05T16:29:49Z","closed_at":null,"body":"Just creating this issue to put some code I have written up here for discussion.\r\n\r\n``` haskell\r\n{-# LANGUAGE DataKinds #-}\r\n{-# LANGUAGE TypeFamilies #-}\r\n{-# LANGUAGE TypeOperators #-}\r\n{-# LANGUAGE FlexibleInstances #-}\r\n{-# LANGUAGE ScopedTypeVariables #-}\r\n{-# LANGUAGE TypeSynonymInstances #-}\r\nmodule Files where\r\n\r\nimport Control.Monad.IO.Class\r\nimport Control.Monad.Trans.Either\r\nimport Control.Monad.Trans.Resource\r\nimport Data.ByteString.Lazy (ByteString)\r\nimport Network.Wai\r\nimport Network.Wai.Handler.Warp (run)\r\nimport Network.Wai.Parse\r\nimport Servant\r\nimport Servant.Server.Internal\r\n\r\n-- Backends for file upload: in memory or in /tmp ?\r\n\r\ndata Mem\r\ndata Tmp\r\n\r\nclass KnownBackend b where\r\n  type Storage b :: *\r\n  \r\n  withBackend :: Proxy b -> (BackEnd (Storage b) -> IO r) -> IO r\r\n\r\ninstance KnownBackend Mem where\r\n  type Storage Mem = ByteString\r\n\r\n  withBackend Proxy f = f lbsBackEnd\r\n\r\ninstance KnownBackend Tmp where\r\n  type Storage Tmp = FilePath\r\n\r\n  withBackend Proxy f = runResourceT . withInternalState $ \\s ->\r\n    f (tempFileBackEnd s)\r\n\r\n-- * Files combinator, to get all of the uploaded files\r\n\r\ndata Files b\r\n\r\ninstance (KnownBackend b, HasServer api) => HasServer (Files b :> api) where\r\n  type ServerT (Files b :> api) m =\r\n    [File (Storage b)] -> ServerT api m\r\n\r\n  route Proxy subserver req respond = withBackend pb $ \\b -> do\r\n    (_, files) <- parseRequestBody b req\r\n    route (Proxy :: Proxy api) (subserver files) req respond\r\n\r\n    where pb = Proxy :: Proxy b\r\n\r\ntype FilesMem = Files Mem\r\ntype FilesTmp = Files Tmp\r\n\r\n-- test\r\n\r\ntype API = \"files\" :> FilesTmp :> Post '[JSON] ()\r\n      :<|> Raw\r\n\r\napi :: Proxy API\r\napi = Proxy\r\n\r\nserver :: Server API\r\nserver = filesHandler :<|> serveDirectory \".\"\r\n\r\n  where filesHandler :: [File FilePath] -> EitherT ServantErr IO ()\r\n        filesHandler = liftIO . mapM_ ppFile\r\n\r\n        ppFile :: File FilePath -> IO ()\r\n        ppFile (name, fileinfo) = do\r\n          putStrLn $ \"Input name: \" ++ show name\r\n          putStrLn $ \"File name: \" ++ show (fileName fileinfo)\r\n          putStrLn $ \"Content type: \" ++ show (fileContentType fileinfo)\r\n          putStrLn $ \"------- Content --------\"\r\n          readFile (fileContent fileinfo) >>= putStrLn\r\n          putStrLn $ \"------------------------\"\r\n\r\napp :: Application\r\napp = serve api server\r\n\r\nf :: IO ()\r\nf = run 8083 app\r\n```\r\n\r\nalong with this HTML file:\r\n\r\n``` html\r\n<form action=\"/files\" method=\"post\" enctype=\"multipart/form-data\">\r\n\tSelect a file: <input type=\"file\" name=\"blah\" />\r\n\tSelect another one: <input type=\"file\" name=\"foo\" />\r\n\t<hr />\r\n\t<input type=\"submit\" value=\"Upload\" />\r\n</form>\r\n```\r\n\r\nserved through `serveDirectory`. Thoughts, comments?"},"comment":{"url":"https://api.github.com/repos/haskell-servant/servant/issues/comments/128060271","html_url":"https://github.com/haskell-servant/servant/issues/133#issuecomment-128060271","issue_url":"https://api.github.com/repos/haskell-servant/servant/issues/133","id":128060271,"user":{"login":"mwotton","id":68482,"avatar_url":"https://avatars.githubusercontent.com/u/68482?v=3","gravatar_id":"","url":"https://api.github.com/users/mwotton","html_url":"https://github.com/mwotton","followers_url":"https://api.github.com/users/mwotton/followers","following_url":"https://api.github.com/users/mwotton/following{/other_user}","gists_url":"https://api.github.com/users/mwotton/gists{/gist_id}","starred_url":"https://api.github.com/users/mwotton/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mwotton/subscriptions","organizations_url":"https://api.github.com/users/mwotton/orgs","repos_url":"https://api.github.com/users/mwotton/repos","events_url":"https://api.github.com/users/mwotton/events{/privacy}","received_events_url":"https://api.github.com/users/mwotton/received_events","type":"User","site_admin":false},"created_at":"2015-08-05T16:29:49Z","updated_at":"2015-08-05T16:29:49Z","body":"i think this is actually a warp thing, i get it when i use scotty too."}}