http-client 0.4.8.1 → 0.4.9
raw patch · 7 files changed
+117/−2 lines, 7 filesdep +directory
Dependencies added: directory
Files
- ChangeLog.md +4/−0
- Network/HTTP/Client.hs +3/−0
- Network/HTTP/Client/MultipartFormData.hs +1/−1
- Network/HTTP/Client/Request.hs +45/−0
- Network/HTTP/Client/Types.hs +11/−0
- http-client.cabal +3/−1
- test-nonet/Network/HTTP/Client/RequestBodySpec.hs +50/−0
ChangeLog.md view
@@ -1,3 +1,7 @@+## 0.4.9++* Add RequestBody smart constructors `streamFile` and `streamFileObserved`, the latter with accompanying type `StreamFileStatus`.+ ## 0.4.8.1 * Automatically call withSocketsDo everywhere [#107](https://github.com/snoyberg/http-client/issues/107)
Network/HTTP/Client.hs view
@@ -146,6 +146,9 @@ , Popper , NeedsPopper , GivesPopper+ , streamFile+ , observedStreamFile+ , StreamFileStatus (..) -- * Response , Response , responseStatus
Network/HTTP/Client/MultipartFormData.hs view
@@ -50,7 +50,7 @@ ,renderPart ) where -import Network.HTTP.Client+import Network.HTTP.Client hiding (streamFile) import Network.Mime import Network.HTTP.Types (hContentType, methodPost, Header()) import Data.Monoid ((<>))
Network/HTTP/Client/Request.hs view
@@ -21,12 +21,16 @@ , requestBuilder , useDefaultTimeout , setQueryString+ , streamFile+ , observedStreamFile ) where +import Data.Int (Int64) import Data.Maybe (fromMaybe, isJust) import Data.Monoid (mempty, mappend) import Data.String (IsString(..)) import Data.Char (toLower)+import Control.Applicative ((<$>)) import Control.Monad (when, unless) import Numeric (showHex) @@ -38,6 +42,7 @@ import qualified Data.ByteString as S import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Lazy as L+import Data.ByteString.Lazy.Internal (defaultChunkSize) import qualified Network.HTTP.Types as W import Network.URI (URI (..), URIAuth (..), parseURI, relativeTo, escapeURIString, isAllowedInURI)@@ -57,6 +62,9 @@ import Control.Monad.Catch (MonadThrow, throwM) import Data.IORef +import System.IO (withBinaryFile, hTell, hFileSize, Handle, IOMode (ReadMode))++ -- | Convert a URL into a 'Request'. -- -- This defaults some of the values in 'Request', such as setting 'method' to@@ -406,3 +414,40 @@ -- Since 0.3.6 setQueryString :: [(S.ByteString, Maybe S.ByteString)] -> Request -> Request setQueryString qs req = req { queryString = W.renderQuery True qs }++-- | Send a file as the request body.+--+-- It is expected that the file size does not change between calling+-- `streamFile` and making any requests using this request body.+--+-- Since 0.4.9+streamFile :: FilePath -> IO RequestBody+streamFile = observedStreamFile (\_ -> return ())++-- | Send a file as the request body, while observing streaming progress via+-- a `PopObserver`. Observations are made between reading and sending a chunk.+--+-- It is expected that the file size does not change between calling+-- `observedStreamFile` and making any requests using this request body.+--+-- Since 0.4.9+observedStreamFile :: (StreamFileStatus -> IO ()) -> FilePath -> IO RequestBody+observedStreamFile obs path = do+ size <- fromIntegral <$> withBinaryFile path ReadMode hFileSize++ let filePopper :: Handle -> Popper+ filePopper h = do+ bs <- S.hGetSome h defaultChunkSize+ currentPosition <- fromIntegral <$> hTell h+ obs $ StreamFileStatus+ { fileSize = size+ , readSoFar = currentPosition+ , thisChunkSize = S.length bs+ }+ return bs++ givesFilePopper :: GivesPopper ()+ givesFilePopper k = withBinaryFile path ReadMode $ \h -> do+ k (filePopper h)++ return $ RequestBodyStream size givesFilePopper
Network/HTTP/Client/Types.hs view
@@ -28,6 +28,7 @@ , ConnHost (..) , ConnKey (..) , ProxyOverride (..)+ , StreamFileStatus (..) ) where import qualified Data.Typeable as T (Typeable)@@ -597,4 +598,14 @@ -- | @ConnKey@ consists of a hostname, a port and a @Bool@ -- specifying whether to use SSL. data ConnKey = ConnKey ConnHost Int S.ByteString Int Bool+ deriving (Eq, Show, Ord, T.Typeable)++-- | Status of streaming a request body from a file.+--+-- Since 0.4.9+data StreamFileStatus = StreamFileStatus+ { fileSize :: Int64+ , readSoFar :: Int64+ , thisChunkSize :: Int+ } deriving (Eq, Show, Ord, T.Typeable)
http-client.cabal view
@@ -1,5 +1,5 @@ name: http-client-version: 0.4.8.1+version: 0.4.9 synopsis: An HTTP client engine, intended as a base layer for more user-friendly packages. description: Hackage documentation generation is not reliable. For up to date documentation, please see: <http://www.stackage.org/package/http-client>. homepage: https://github.com/snoyberg/http-client@@ -95,6 +95,7 @@ Network.HTTP.Client.BodySpec Network.HTTP.Client.HeadersSpec Network.HTTP.Client.RequestSpec+ Network.HTTP.Client.RequestBodySpec build-depends: base , http-client , hspec@@ -113,3 +114,4 @@ , zlib , async , streaming-commons >= 0.1.1+ , directory
+ test-nonet/Network/HTTP/Client/RequestBodySpec.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE OverloadedStrings #-}+module Network.HTTP.Client.RequestBodySpec where++import Control.Monad+import Test.Hspec+import Control.Exception+import System.IO+import Data.IORef+import qualified Data.ByteString as BS+import Network.HTTP.Client (streamFile, parseUrl, requestBody)+import Network.HTTP.Client.Internal (dummyConnection, Connection, connectionWrite, requestBuilder)+import System.Directory (getTemporaryDirectory)++spec :: Spec+spec = describe "streamFile" $ it "works" $ withTmpFile $ \(path, h) -> do+ replicateM_ 5000 $ BS.hPut h "Hello, world!\r\n"+ hClose h++ withBinaryFile path ReadMode $ \h' -> do+ conn <- verifyFileConnection h'++ req0 <- parseUrl "http://example.com"+ body <- streamFile path+ let req = req0 { requestBody = body }++ requestBuilder req conn+ hIsEOF h' `shouldReturn` True+ where+ withTmpFile = bracket getTmpFile closeTmpFile+ getTmpFile = do+ tmp <- getTemporaryDirectory+ openBinaryTempFile tmp "request-body-stream-file"+ closeTmpFile (_, h) = hClose h++ firstReadBS = "GET / HTTP/1.1\r\nHost: example.com\r\nAccept-Encoding: gzip\r\nContent-Length: 75000\r\n\r\n"++ verifyFileConnection h = do+ (conn, _, _) <- dummyConnection []+ isFirstReadRef <- newIORef True+ return conn+ { connectionWrite = \bs -> do+ isFirstRead <- readIORef isFirstReadRef+ if isFirstRead+ then do+ bs `shouldBe` firstReadBS+ writeIORef isFirstReadRef False+ else do+ bs' <- BS.hGet h (BS.length bs)+ bs `shouldBe` bs'+ }