add file upload recipe
This commit is contained in:
parent
6dc9d04e60
commit
5ac6de8277
4 changed files with 219 additions and 0 deletions
|
@ -9,6 +9,7 @@ packages: servant/
|
||||||
doc/cookbook/db-postgres-pool/
|
doc/cookbook/db-postgres-pool/
|
||||||
doc/cookbook/db-sqlite-simple/
|
doc/cookbook/db-sqlite-simple/
|
||||||
doc/cookbook/jwt-and-basic-auth/
|
doc/cookbook/jwt-and-basic-auth/
|
||||||
|
doc/cookbook/file-upload/
|
||||||
|
|
||||||
allow-newer: servant-js:servant-foreign
|
allow-newer: servant-js:servant-foreign
|
||||||
|
|
||||||
|
|
187
doc/cookbook/file-upload/FileUpload.lhs
Normal file
187
doc/cookbook/file-upload/FileUpload.lhs
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
# File Upload (`multipart/form-data`)
|
||||||
|
|
||||||
|
In this recipe, we will implement a web application
|
||||||
|
with a single endpoint that can process
|
||||||
|
`multipart/form-data` request bodies, which most
|
||||||
|
commonly come from HTML forms that allow file upload.
|
||||||
|
|
||||||
|
As usual, a bit of throat clearing.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
import Control.Concurrent
|
||||||
|
import Control.Exception
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.IO.Class
|
||||||
|
import Data.Text.Encoding (encodeUtf8)
|
||||||
|
import Network (withSocketsDo)
|
||||||
|
import Network.HTTP.Client hiding (Proxy)
|
||||||
|
import Network.HTTP.Client.MultipartFormData
|
||||||
|
import Network.Wai.Handler.Warp
|
||||||
|
import Servant
|
||||||
|
import Servant.Multipart
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
```
|
||||||
|
|
||||||
|
Our API consists in a single `POST` endpoint at `/`
|
||||||
|
that takes a `multipart/form-data` request body and
|
||||||
|
pretty-prints the data it got to stdout before returning `0`
|
||||||
|
(because why not).
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
type API = MultipartForm Mem (MultipartData Mem) :> Post '[JSON] Integer
|
||||||
|
|
||||||
|
api :: Proxy API
|
||||||
|
api = Proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
Because of some technicalities, multipart form data is not
|
||||||
|
represented as a good old content type like `JSON` in servant,
|
||||||
|
that one could use with `ReqBody`, but instead is its own
|
||||||
|
dedicated `ReqBody`-like combinator named
|
||||||
|
[`MultiPartForm`](https://hackage.haskell.org/package/servant-multipart-0.11/docs/Servant-Multipart.html#t:MultipartForm).
|
||||||
|
|
||||||
|
This combinator takes two parameters. The first one is the
|
||||||
|
"backend" to use. Currently, you only have the choice between
|
||||||
|
`Mem` and `Tmp`. The former loads the entire input in memory,
|
||||||
|
even the uploadedd files, while `Tmp` will stream uploaded
|
||||||
|
files to some temporary directory.
|
||||||
|
|
||||||
|
The second parameter is the type you want the multipart data
|
||||||
|
to be decoded to. Indeed there is a `FromMultipart` class that
|
||||||
|
allows you to specify how to decode multipart form data from
|
||||||
|
`MultipartData` to a custom type of yours. Here we use the
|
||||||
|
trivial "decoding" to `MultipartData` itself, and simply
|
||||||
|
will get our hands on the raw input. If you want to use
|
||||||
|
a type of yours, see the documentation for
|
||||||
|
[`FromMultipart`](https://hackage.haskell.org/package/servant-multipart-0.11/docs/Servant-Multipart.html#t:FromMultipart).
|
||||||
|
|
||||||
|
Our only request handler has type `MultipartData Mem -> Handler Integer`.
|
||||||
|
All it does is list the textual and file inputs that
|
||||||
|
were sent in the multipart request body. The textual
|
||||||
|
inputs are in the `inputs` field while the file inputs
|
||||||
|
are in the `files` field of `multipartData`.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
-- MultipartData consists in textual inputs,
|
||||||
|
-- accessible through its "inputs" field, as well
|
||||||
|
-- as files, accessible through its "files" field.
|
||||||
|
upload :: Server API
|
||||||
|
upload multipartData = do
|
||||||
|
liftIO $ do
|
||||||
|
putStrLn "Inputs:"
|
||||||
|
forM_ (inputs multipartData) $ \input ->
|
||||||
|
putStrLn $ " " ++ show (iName input)
|
||||||
|
++ " -> " ++ show (iValue input)
|
||||||
|
|
||||||
|
forM_ (files multipartData) $ \file -> do
|
||||||
|
let content = fdPayload file
|
||||||
|
putStrLn $ "Content of " ++ show (fdFileName file)
|
||||||
|
LBS.putStr content
|
||||||
|
return 0
|
||||||
|
|
||||||
|
startServer :: IO ()
|
||||||
|
startServer = run 8080 (serve api upload)
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, a main function that brings up our server and
|
||||||
|
sends some test request with `http-client` (and not
|
||||||
|
servant-client this time, has servant-multipart does not
|
||||||
|
yet have support for client generation.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
main :: IO ()
|
||||||
|
main = withSocketsDo . bracket (forkIO startServer) killThread $ \_threadid -> do
|
||||||
|
-- we fork the server in a separate thread and send a test
|
||||||
|
-- request to it from the main thread.
|
||||||
|
manager <- newManager defaultManagerSettings
|
||||||
|
req <- parseRequest "http://localhost:8080/"
|
||||||
|
resp <- flip httpLbs manager =<< formDataBody form req
|
||||||
|
print resp
|
||||||
|
|
||||||
|
where form =
|
||||||
|
[ partBS "title" "World"
|
||||||
|
, partBS "text" $ encodeUtf8 "Hello"
|
||||||
|
, partFileSource "file" "./README.md"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
If you run this, you should get:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cabal new-build cookbook-file-upload
|
||||||
|
[...]
|
||||||
|
$ dist-newstyle/build/x86_64-linux/ghc-8.2.1/cookbook-file-upload-0.1/x/cookbook-file-upload/build/cookbook-file-upload/cookbook-file-upload
|
||||||
|
Inputs:
|
||||||
|
"title" -> "World"
|
||||||
|
"text" -> "Hello"
|
||||||
|
Content of "README.md"
|
||||||
|
# servant - A Type-Level Web DSL
|
||||||
|
|
||||||
|
![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
We have a [tutorial](http://haskell-servant.readthedocs.org/en/stable/tutorial/index.html) that
|
||||||
|
introduces the core features of servant. After this article, you should be able
|
||||||
|
to write your first servant webservices, learning the rest from the haddocks'
|
||||||
|
examples.
|
||||||
|
|
||||||
|
The central documentation can be found [here](http://haskell-servant.readthedocs.org/).
|
||||||
|
Other blog posts, videos and slides can be found on the
|
||||||
|
[website](http://haskell-servant.github.io/).
|
||||||
|
|
||||||
|
If you need help, drop by the IRC channel (#servant on freenode) or [mailing
|
||||||
|
list](https://groups.google.com/forum/#!forum/haskell-servant).
|
||||||
|
|
||||||
|
## Version history
|
||||||
|
|
||||||
|
This table lists the versions of some `servant-` libraries at the point of
|
||||||
|
release of `servant` package.
|
||||||
|
|
||||||
|
| | **0.10** | **0.11** | **0.12** |
|
||||||
|
| ------------------- | -------- |----------|----------|
|
||||||
|
| servant | 0.10 | 0.11 | 0.12 |
|
||||||
|
| servant-blaze | 0.7.1 | ? | ? |
|
||||||
|
| servant-cassava | 0.7 | ? | ? |
|
||||||
|
| servant-client | 0.10 | 0.11 | 0.12 |
|
||||||
|
| servant-docs | 0.10 | 0.11 | 0.11.1 |
|
||||||
|
| servant-foreign | 0.10 | 0.10.0.1 | 0.10.2 |
|
||||||
|
| servant-js | 0.9.1 | ? | ? |
|
||||||
|
| servant-lucid | 0.7.1 | ? | ? |
|
||||||
|
| servant-mock | 0.8.1.1 | ? | ? |
|
||||||
|
| servant-server | 0.10 | 0.11 | 0.12 |
|
||||||
|
| servant-swagger | 1.1.2.1 | ? | ? |
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See `CONTRIBUTING.md`
|
||||||
|
|
||||||
|
## Release process outline (by phadej)
|
||||||
|
|
||||||
|
- Update changelog and bump versions in `master`
|
||||||
|
- `git log --oneline v0.12.. | grep 'Merge pull request'` is a good starting point (use correct previous release tag)
|
||||||
|
- Create a release branch, e.g. `release-0.13`, and *protect it* from accidental force pushes.
|
||||||
|
- Release branch is useful for backporting fixes from `master`
|
||||||
|
- Smoke test in [`servant-universe`](https://github.com/phadej/servant-universe)
|
||||||
|
- `git submodule foreach git checkout master` and `git submodule foreach git pull` to get newest of everything.
|
||||||
|
- `cabal new-build --enable-tests all` to verify that everything builds, and `cabal new-test all` to run tests
|
||||||
|
- It's a good idea to separate these steps, as tests often pass, if they compile :)
|
||||||
|
- See `cabal.project` to selectively `allow-newer`
|
||||||
|
- If some packages are broken, on your discretisation there are two options:
|
||||||
|
- Fix them and make PRs: it's good idea to test against older `servant` version too.
|
||||||
|
- Temporarily comment out broken package
|
||||||
|
- If you make a commit for `servant-universe`, you can use it as submodule in private projects to test even more
|
||||||
|
- When ripples are cleared out:
|
||||||
|
- `git tag -s` the release
|
||||||
|
- `git push --tags`
|
||||||
|
- `cabal sdist` and `cabal upload`
|
||||||
|
Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("Transfer-Encoding","chunked"),("Date","Fri, 08 Dec 2017 16:50:14 GMT"),("Server","Warp/3.2.13"),("Content-Type","application/json;charset=utf-8")], responseBody = "0", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
|
||||||
|
```
|
||||||
|
|
||||||
|
As usual, the code for this recipe is available in a cabal
|
||||||
|
project [here]().
|
30
doc/cookbook/file-upload/file-upload.cabal
Normal file
30
doc/cookbook/file-upload/file-upload.cabal
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
name: cookbook-file-upload
|
||||||
|
version: 0.1
|
||||||
|
synopsis: File upload cookbook example
|
||||||
|
homepage: http://haskell-servant.readthedocs.org/
|
||||||
|
license: BSD3
|
||||||
|
license-file: ../../../servant/LICENSE
|
||||||
|
author: Servant Contributors
|
||||||
|
maintainer: haskell-servant-maintainers@googlegroups.com
|
||||||
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
|
|
||||||
|
executable cookbook-file-upload
|
||||||
|
if impl(ghc < 7.10.1)
|
||||||
|
buildable: False
|
||||||
|
main-is: FileUpload.lhs
|
||||||
|
build-depends: base == 4.*
|
||||||
|
, text
|
||||||
|
, mtl
|
||||||
|
, network
|
||||||
|
, bytestring
|
||||||
|
, servant
|
||||||
|
, servant-server
|
||||||
|
, servant-multipart
|
||||||
|
, warp
|
||||||
|
, wai
|
||||||
|
, markdown-unlit >= 0.4
|
||||||
|
, http-client
|
||||||
|
default-language: Haskell2010
|
||||||
|
ghc-options: -Wall -pgmL markdown-unlit
|
||||||
|
build-tool-depends: markdown-unlit:markdown-unlit
|
|
@ -21,3 +21,4 @@ you name it!
|
||||||
db-postgres-pool/PostgresPool.lhs
|
db-postgres-pool/PostgresPool.lhs
|
||||||
basic-auth/BasicAuth.lhs
|
basic-auth/BasicAuth.lhs
|
||||||
jwt-and-basic-auth/JWTAndBasicAuth.lhs
|
jwt-and-basic-auth/JWTAndBasicAuth.lhs
|
||||||
|
file-upload/FileUpload.lhs
|
||||||
|
|
Loading…
Reference in a new issue