{-# LANGUAGE OverloadedStrings #-}
module Network.HTTP.Client.MultipartFormData
(
Part
,PartM
,partName
,partFilename
,partContentType
,partHeaders
,partGetBody
,partBS
,partLBS
,partFile
,partFileSource
,partFileSourceChunked
,partFileRequestBody
,partFileRequestBodyM
,addPartHeaders
,formDataBody
,formDataBodyWithBoundary
,webkitBoundary
,webkitBoundaryPure
,renderParts
,renderPart
) where
import Network.HTTP.Client hiding (streamFile)
import Network.Mime
import Network.HTTP.Types (hContentType, methodPost, Header())
import Data.Monoid ((<>))
import Data.Foldable (foldMap)
import Blaze.ByteString.Builder
import Data.Text
import qualified Data.Text.Encoding as TE
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString as BS
import qualified Data.CaseInsensitive as CI
import Control.Monad.Trans.State.Strict (state, runState)
import Control.Monad.IO.Class
import System.FilePath
import System.Random
import Data.Array.Base
import System.IO
import Data.Bits
import Data.Word
import Data.Monoid (Monoid(..))
import Control.Monad
import Data.ByteString.Lazy.Internal (defaultChunkSize)
type Part = PartM IO
data PartM m = Part
{ partName :: Text
, partFilename :: Maybe String
, partContentType :: Maybe MimeType
, partHeaders :: [Header]
, partGetBody :: m RequestBody
}
instance Show (PartM m) where
showsPrec d (Part n f c h _) =
showParen (d>=11) $ showString "Part "
. showsPrec 11 n
. showString " "
. showsPrec 11 f
. showString " "
. showsPrec 11 c
. showString " "
. showsPrec 11 h
. showString " "
. showString "<m (RequestBody m)>"
partBS :: Applicative m
=> Text
-> BS.ByteString
-> PartM m
partBS n b = Part n Data.Monoid.mempty mempty mempty $ pure $ RequestBodyBS b
partLBS :: Applicative m
=> Text
-> BL.ByteString
-> PartM m
partLBS n b = Part n mempty mempty mempty $ pure $ RequestBodyLBS b
partFile :: Text
-> FilePath
-> Part
partFile n f =
partFileRequestBodyM n f $ do
liftM RequestBodyBS $ liftIO $ BS.readFile f
partFileSource :: Text
-> FilePath
-> Part
partFileSource n f =
partFileRequestBodyM n f $ do
size <- liftIO $ withBinaryFile f ReadMode hFileSize
return $ RequestBodyStream (fromInteger size) $ streamFile f
streamFile :: FilePath -> GivesPopper ()
streamFile fp np =
withFile fp ReadMode $ np . go
where
go h = BS.hGetSome h defaultChunkSize
partFileSourceChunked :: Text -> FilePath -> Part
partFileSourceChunked n f =
partFileRequestBody n f $ do
RequestBodyStreamChunked $ streamFile f
partFileRequestBody :: Applicative m
=> Text
-> FilePath
-> RequestBody
-> PartM m
partFileRequestBody n f rqb =
partFileRequestBodyM n f $ pure rqb
partFileRequestBodyM :: Text
-> FilePath
-> m RequestBody
-> PartM m
partFileRequestBodyM n f rqb =
Part n (Just f) (Just $ defaultMimeLookup $ pack f) mempty rqb
{-# INLINE cp #-}
cp :: BS.ByteString -> RequestBody
cp bs = RequestBodyBuilder (fromIntegral $ BS.length bs) $ copyByteString bs
addPartHeaders :: PartM m -> [Header] -> PartM m
addPartHeaders p hs = p { partHeaders = partHeaders p <> hs }
renderPart :: Functor m
=> BS.ByteString
-> PartM m -> m RequestBody
renderPart boundary (Part name mfilename mcontenttype hdrs get) = render <$> get
where render renderBody =
cp "--" <> cp boundary <> cp "\r\n"
<> cp "Content-Disposition: form-data; name=\""
<> RequestBodyBS (TE.encodeUtf8 name)
<> (case mfilename of
Just f -> cp "\"; filename=\""
<> RequestBodyBS (TE.encodeUtf8 $ pack $ takeFileName f)
_ -> mempty)
<> cp "\""
<> (case mcontenttype of
Just ct -> cp "\r\n"
<> cp "Content-Type: "
<> cp ct
_ -> mempty)
<> Data.Foldable.foldMap (\(k, v) ->
cp "\r\n"
<> cp (CI.original k)
<> cp ": "
<> cp v) hdrs
<> cp "\r\n\r\n"
<> renderBody <> cp "\r\n"
renderParts :: Applicative m
=> BS.ByteString
-> [PartM m] -> m RequestBody
renderParts boundary parts = (fin . mconcat) <$> traverse (renderPart boundary) parts
where fin = (<> cp "--" <> cp boundary <> cp "--\r\n")
webkitBoundary :: IO BS.ByteString
webkitBoundary = getStdRandom webkitBoundaryPure
webkitBoundaryPure :: RandomGen g => g -> (BS.ByteString, g)
webkitBoundaryPure g = (`runState` g) $ do
fmap (BS.append prefix . BS.pack . Prelude.concat) $ replicateM 4 $ do
randomness <- state $ random
return [unsafeAt alphaNumericEncodingMap $ randomness `shiftR` 24 .&. 0x3F
,unsafeAt alphaNumericEncodingMap $ randomness `shiftR` 16 .&. 0x3F
,unsafeAt alphaNumericEncodingMap $ randomness `shiftR` 8 .&. 0x3F
,unsafeAt alphaNumericEncodingMap $ randomness .&. 0x3F]
where
prefix = "----WebKitFormBoundary"
alphaNumericEncodingMap :: UArray Int Word8
alphaNumericEncodingMap = listArray (0, 63)
[0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50,
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
0x77, 0x78, 0x79, 0x7A, 0x30, 0x31, 0x32, 0x33,
0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42]
formDataBody :: MonadIO m => [Part] -> Request -> m Request
formDataBody a b = liftIO $ do
boundary <- webkitBoundary
formDataBodyWithBoundary boundary a b
formDataBodyWithBoundary :: Applicative m => BS.ByteString -> [PartM m] -> Request -> m Request
formDataBodyWithBoundary boundary parts req = do
(\ body -> req
{ method = methodPost
, requestHeaders =
(hContentType, "multipart/form-data; boundary=" <> boundary)
: Prelude.filter (\(x, _) -> x /= hContentType) (requestHeaders req)
, requestBody = body
}) <$> renderParts boundary parts