2011-08-20 20:11:42 +00:00
|
|
|
{- Url downloading.
|
2011-08-17 00:49:04 +00:00
|
|
|
-
|
2018-04-04 19:15:12 +00:00
|
|
|
- Copyright 2011-2018 Joey Hess <id@joeyh.name>
|
2011-08-17 00:49:04 +00:00
|
|
|
-
|
2014-05-10 14:01:27 +00:00
|
|
|
- License: BSD-2-clause
|
2011-08-17 00:49:04 +00:00
|
|
|
-}
|
|
|
|
|
2012-10-10 15:26:30 +00:00
|
|
|
{-# LANGUAGE CPP #-}
|
2014-08-15 22:02:17 +00:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2014-08-17 19:39:01 +00:00
|
|
|
{-# LANGUAGE RankNTypes #-}
|
2015-05-10 19:37:55 +00:00
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
2012-10-10 15:26:30 +00:00
|
|
|
|
2011-08-20 20:11:42 +00:00
|
|
|
module Utility.Url (
|
2018-04-04 19:15:12 +00:00
|
|
|
newManager,
|
2015-10-15 14:34:19 +00:00
|
|
|
managerSettings,
|
2012-01-02 18:20:20 +00:00
|
|
|
URLString,
|
2013-09-28 18:35:21 +00:00
|
|
|
UserAgent,
|
2018-04-04 19:15:12 +00:00
|
|
|
UrlOptions(..),
|
|
|
|
defUrlOptions,
|
2014-08-15 21:47:21 +00:00
|
|
|
mkUrlOptions,
|
2012-02-10 23:17:41 +00:00
|
|
|
check,
|
2013-10-11 17:05:00 +00:00
|
|
|
checkBoth,
|
2011-08-17 00:49:04 +00:00
|
|
|
exists,
|
2015-01-22 18:52:52 +00:00
|
|
|
UrlInfo(..),
|
|
|
|
getUrlInfo,
|
2015-08-19 16:24:55 +00:00
|
|
|
assumeUrlExists,
|
2011-08-17 00:49:04 +00:00
|
|
|
download,
|
2018-04-06 19:58:16 +00:00
|
|
|
sinkResponseFile,
|
2017-12-06 17:16:06 +00:00
|
|
|
downloadPartial,
|
2016-07-12 20:30:36 +00:00
|
|
|
parseURIRelaxed,
|
|
|
|
matchStatusCodeException,
|
2017-09-12 19:13:42 +00:00
|
|
|
matchHttpExceptionContent,
|
2011-08-17 00:49:04 +00:00
|
|
|
) where
|
|
|
|
|
2012-03-16 00:39:25 +00:00
|
|
|
import Common
|
2018-04-06 19:58:16 +00:00
|
|
|
import Utility.Metered
|
2015-05-05 17:53:06 +00:00
|
|
|
|
2011-08-17 00:49:04 +00:00
|
|
|
import Network.URI
|
2014-08-15 21:17:19 +00:00
|
|
|
import Network.HTTP.Types
|
|
|
|
import qualified Data.CaseInsensitive as CI
|
2014-08-15 22:02:17 +00:00
|
|
|
import qualified Data.ByteString as B
|
2014-08-15 21:17:19 +00:00
|
|
|
import qualified Data.ByteString.UTF8 as B8
|
2017-12-06 17:16:06 +00:00
|
|
|
import qualified Data.ByteString.Lazy as L
|
2015-10-01 17:47:54 +00:00
|
|
|
import Control.Monad.Trans.Resource
|
2018-04-04 19:15:12 +00:00
|
|
|
import Network.HTTP.Conduit
|
2017-12-06 17:16:06 +00:00
|
|
|
import Network.HTTP.Client (brRead, withResponse)
|
2018-04-06 19:58:16 +00:00
|
|
|
import Data.Conduit
|
2015-10-01 17:47:54 +00:00
|
|
|
|
2017-08-17 15:00:48 +00:00
|
|
|
#if ! MIN_VERSION_http_client(0,5,0)
|
|
|
|
responseTimeoutNone :: Maybe Int
|
|
|
|
responseTimeoutNone = Nothing
|
|
|
|
#endif
|
|
|
|
|
2015-10-15 14:34:19 +00:00
|
|
|
managerSettings :: ManagerSettings
|
|
|
|
#if MIN_VERSION_http_conduit(2,1,7)
|
|
|
|
managerSettings = tlsManagerSettings
|
|
|
|
#else
|
|
|
|
managerSettings = conduitManagerSettings
|
|
|
|
#endif
|
2017-08-15 17:56:12 +00:00
|
|
|
{ managerResponseTimeout = responseTimeoutNone }
|
2015-10-15 14:34:19 +00:00
|
|
|
|
2011-08-17 00:49:04 +00:00
|
|
|
type URLString = String
|
|
|
|
|
2012-04-22 05:13:09 +00:00
|
|
|
type Headers = [String]
|
|
|
|
|
2013-09-28 18:35:21 +00:00
|
|
|
type UserAgent = String
|
|
|
|
|
2014-08-15 21:47:21 +00:00
|
|
|
data UrlOptions = UrlOptions
|
2014-02-25 02:00:25 +00:00
|
|
|
{ userAgent :: Maybe UserAgent
|
|
|
|
, reqHeaders :: Headers
|
2018-04-06 21:00:46 +00:00
|
|
|
, urlDownloader :: UrlDownloader
|
2014-08-15 21:47:21 +00:00
|
|
|
, applyRequest :: Request -> Request
|
2018-04-04 19:15:12 +00:00
|
|
|
, httpManager :: Manager
|
2014-02-25 02:00:25 +00:00
|
|
|
}
|
|
|
|
|
2018-04-06 21:00:46 +00:00
|
|
|
data UrlDownloader
|
|
|
|
= DownloadWithConduit
|
|
|
|
| DownloadWithCurl [CommandParam]
|
|
|
|
|
2018-04-04 19:15:12 +00:00
|
|
|
defUrlOptions :: IO UrlOptions
|
|
|
|
defUrlOptions = UrlOptions
|
|
|
|
<$> pure Nothing
|
|
|
|
<*> pure []
|
2018-04-06 21:00:46 +00:00
|
|
|
<*> pure DownloadWithConduit
|
2018-04-04 19:15:12 +00:00
|
|
|
<*> pure id
|
|
|
|
<*> newManager managerSettings
|
|
|
|
|
|
|
|
mkUrlOptions :: Maybe UserAgent -> Headers -> [CommandParam] -> Manager -> UrlOptions
|
|
|
|
mkUrlOptions defuseragent reqheaders reqparams manager =
|
2018-04-06 21:00:46 +00:00
|
|
|
UrlOptions useragent reqheaders urldownloader applyrequest manager
|
2014-08-15 21:17:19 +00:00
|
|
|
where
|
2018-04-06 21:00:46 +00:00
|
|
|
urldownloader = if null reqparams
|
2018-05-21 15:00:23 +00:00
|
|
|
#if MIN_VERSION_cryptonite(0,6,0)
|
2018-04-06 21:00:46 +00:00
|
|
|
then DownloadWithConduit
|
2018-05-20 18:12:18 +00:00
|
|
|
#else
|
|
|
|
-- Work around for old cryptonite bug that broke tls.
|
|
|
|
-- https://github.com/vincenthz/hs-tls/issues/109
|
|
|
|
then DownloadWithCurl reqparams
|
|
|
|
#endif
|
2018-04-06 21:00:46 +00:00
|
|
|
else DownloadWithCurl reqparams
|
2014-08-15 21:17:19 +00:00
|
|
|
applyrequest = \r -> r { requestHeaders = requestHeaders r ++ addedheaders }
|
|
|
|
addedheaders = uaheader ++ otherheaders
|
2016-01-11 16:10:38 +00:00
|
|
|
useragent = maybe defuseragent (Just . B8.toString . snd)
|
|
|
|
(headMaybe uafromheaders)
|
2014-08-15 21:47:21 +00:00
|
|
|
uaheader = case useragent of
|
2014-08-15 21:17:19 +00:00
|
|
|
Nothing -> []
|
|
|
|
Just ua -> [(hUserAgent, B8.fromString ua)]
|
2016-01-11 16:10:38 +00:00
|
|
|
(uafromheaders, otherheaders) = partition (\(h, _) -> h == hUserAgent)
|
|
|
|
(map toheader reqheaders)
|
2014-08-15 21:17:19 +00:00
|
|
|
toheader s =
|
|
|
|
let (h, v) = separate (== ':') s
|
|
|
|
h' = CI.mk (B8.fromString h)
|
|
|
|
in case v of
|
|
|
|
(' ':v') -> (h', B8.fromString v')
|
|
|
|
_ -> (h', B8.fromString v)
|
|
|
|
|
2018-04-06 21:00:46 +00:00
|
|
|
curlParams :: UrlOptions -> [CommandParam] -> [CommandParam]
|
|
|
|
curlParams uo ps = ps ++ uaparams ++ headerparams ++ addedparams
|
|
|
|
where
|
|
|
|
uaparams = case userAgent uo of
|
|
|
|
Nothing -> []
|
|
|
|
Just ua -> [Param "--user-agent", Param ua]
|
|
|
|
headerparams = concatMap (\h -> [Param "-H", Param h]) (reqHeaders uo)
|
|
|
|
addedparams = case urlDownloader uo of
|
|
|
|
DownloadWithConduit -> []
|
|
|
|
DownloadWithCurl l -> l
|
2014-08-15 21:17:19 +00:00
|
|
|
|
2012-02-10 23:17:41 +00:00
|
|
|
{- Checks that an url exists and could be successfully downloaded,
|
|
|
|
- also checking that its size, if available, matches a specified size. -}
|
2014-02-25 02:00:25 +00:00
|
|
|
checkBoth :: URLString -> Maybe Integer -> UrlOptions -> IO Bool
|
|
|
|
checkBoth url expected_size uo = do
|
|
|
|
v <- check url expected_size uo
|
2013-10-11 17:05:00 +00:00
|
|
|
return (fst v && snd v)
|
2016-12-28 04:17:36 +00:00
|
|
|
|
2014-02-25 02:00:25 +00:00
|
|
|
check :: URLString -> Maybe Integer -> UrlOptions -> IO (Bool, Bool)
|
2018-04-04 19:15:12 +00:00
|
|
|
check url expected_size uo = go <$> getUrlInfo url uo
|
2012-12-13 04:24:19 +00:00
|
|
|
where
|
2015-01-22 18:52:52 +00:00
|
|
|
go (UrlInfo False _ _) = (False, False)
|
|
|
|
go (UrlInfo True Nothing _) = (True, True)
|
|
|
|
go (UrlInfo True s _) = case expected_size of
|
2013-10-11 17:05:00 +00:00
|
|
|
Just _ -> (True, expected_size == s)
|
|
|
|
Nothing -> (True, True)
|
2012-02-10 23:17:41 +00:00
|
|
|
|
2015-01-22 18:52:52 +00:00
|
|
|
exists :: URLString -> UrlOptions -> IO Bool
|
|
|
|
exists url uo = urlExists <$> getUrlInfo url uo
|
|
|
|
|
|
|
|
data UrlInfo = UrlInfo
|
|
|
|
{ urlExists :: Bool
|
|
|
|
, urlSize :: Maybe Integer
|
|
|
|
, urlSuggestedFile :: Maybe FilePath
|
|
|
|
}
|
2016-07-12 20:30:36 +00:00
|
|
|
deriving (Show)
|
2015-01-22 18:52:52 +00:00
|
|
|
|
2015-08-19 16:24:55 +00:00
|
|
|
assumeUrlExists :: UrlInfo
|
|
|
|
assumeUrlExists = UrlInfo True Nothing Nothing
|
|
|
|
|
2012-02-10 23:17:41 +00:00
|
|
|
{- Checks that an url exists and could be successfully downloaded,
|
2015-01-22 18:52:52 +00:00
|
|
|
- also returning its size and suggested filename if available. -}
|
|
|
|
getUrlInfo :: URLString -> UrlOptions -> IO UrlInfo
|
|
|
|
getUrlInfo url uo = case parseURIRelaxed url of
|
2018-04-06 21:00:46 +00:00
|
|
|
Just u -> case (urlDownloader uo, parseUrlConduit (show u)) of
|
|
|
|
(DownloadWithConduit, Just req) -> catchJust
|
2016-07-12 20:30:36 +00:00
|
|
|
-- When http redirects to a protocol which
|
|
|
|
-- conduit does not support, it will throw
|
|
|
|
-- a StatusCodeException with found302.
|
|
|
|
(matchStatusCodeException (== found302))
|
|
|
|
(existsconduit req)
|
|
|
|
(const (existscurl u))
|
|
|
|
`catchNonAsync` (const dne)
|
2014-08-15 21:17:19 +00:00
|
|
|
-- http-conduit does not support file:, ftp:, etc urls,
|
|
|
|
-- so fall back to reading files and using curl.
|
2018-04-06 21:00:46 +00:00
|
|
|
_
|
2014-08-15 21:17:19 +00:00
|
|
|
| uriScheme u == "file:" -> do
|
2015-01-20 20:58:48 +00:00
|
|
|
let f = unEscapeString (uriPath u)
|
|
|
|
s <- catchMaybeIO $ getFileStatus f
|
2014-08-15 21:17:19 +00:00
|
|
|
case s of
|
2015-01-20 20:58:48 +00:00
|
|
|
Just stat -> do
|
|
|
|
sz <- getFileSize' f stat
|
2015-01-22 18:52:52 +00:00
|
|
|
found (Just sz) Nothing
|
2014-08-15 21:17:19 +00:00
|
|
|
Nothing -> dne
|
2018-04-07 01:17:36 +00:00
|
|
|
| otherwise -> existscurl u
|
2013-01-26 22:30:53 +00:00
|
|
|
Nothing -> dne
|
2012-12-13 04:24:19 +00:00
|
|
|
where
|
2015-01-22 18:52:52 +00:00
|
|
|
dne = return $ UrlInfo False Nothing Nothing
|
|
|
|
found sz f = return $ UrlInfo True sz f
|
2013-01-26 22:30:53 +00:00
|
|
|
|
2018-04-06 21:00:46 +00:00
|
|
|
curlparams = curlParams uo $
|
2013-09-28 18:35:21 +00:00
|
|
|
[ Param "-s"
|
|
|
|
, Param "--head"
|
|
|
|
, Param "-L", Param url
|
|
|
|
, Param "-w", Param "%{http_code}"
|
2018-04-06 21:00:46 +00:00
|
|
|
]
|
2013-01-26 22:30:53 +00:00
|
|
|
|
2014-08-15 21:17:19 +00:00
|
|
|
extractlencurl s = case lastMaybe $ filter ("Content-Length:" `isPrefixOf`) (lines s) of
|
2013-01-26 22:30:53 +00:00
|
|
|
Just l -> case lastMaybe $ words l of
|
|
|
|
Just sz -> readish sz
|
|
|
|
_ -> Nothing
|
|
|
|
_ -> Nothing
|
2014-08-15 21:17:19 +00:00
|
|
|
|
2018-04-06 21:00:46 +00:00
|
|
|
extractlen = readish . B8.toString
|
|
|
|
<=< lookup hContentLength . responseHeaders
|
2015-01-22 18:52:52 +00:00
|
|
|
|
|
|
|
extractfilename = contentDispositionFilename . B8.toString
|
2018-04-06 21:00:46 +00:00
|
|
|
<=< lookup hContentDisposition . responseHeaders
|
2015-01-22 18:52:52 +00:00
|
|
|
|
2015-10-01 17:47:54 +00:00
|
|
|
existsconduit req = do
|
2015-01-22 17:47:06 +00:00
|
|
|
let req' = headRequest (applyRequest uo req)
|
2018-04-04 19:15:12 +00:00
|
|
|
runResourceT $ do
|
|
|
|
resp <- http req' (httpManager uo)
|
|
|
|
-- forces processing the response while
|
|
|
|
-- within the runResourceT
|
2015-10-01 17:47:54 +00:00
|
|
|
liftIO $ if responseStatus resp == ok200
|
|
|
|
then found
|
|
|
|
(extractlen resp)
|
|
|
|
(extractfilename resp)
|
|
|
|
else dne
|
2013-09-28 18:35:21 +00:00
|
|
|
|
2016-07-12 20:30:36 +00:00
|
|
|
existscurl u = do
|
|
|
|
output <- catchDefaultIO "" $
|
|
|
|
readProcess "curl" $ toCommand curlparams
|
|
|
|
let len = extractlencurl output
|
|
|
|
let good = found len Nothing
|
|
|
|
let isftp = or
|
|
|
|
[ "ftp" `isInfixOf` uriScheme u
|
|
|
|
-- Check to see if http redirected to ftp.
|
|
|
|
, "Location: ftp://" `isInfixOf` output
|
|
|
|
]
|
|
|
|
case lastMaybe (lines output) of
|
|
|
|
Just ('2':_:_) -> good
|
|
|
|
-- don't try to parse ftp status codes; if curl
|
|
|
|
-- got a length, it's good
|
|
|
|
_ | isftp && isJust len -> good
|
|
|
|
_ -> dne
|
|
|
|
|
2015-01-22 18:52:52 +00:00
|
|
|
-- Parse eg: attachment; filename="fname.ext"
|
|
|
|
-- per RFC 2616
|
|
|
|
contentDispositionFilename :: String -> Maybe FilePath
|
|
|
|
contentDispositionFilename s
|
|
|
|
| "attachment; filename=\"" `isPrefixOf` s && "\"" `isSuffixOf` s =
|
|
|
|
Just $ reverse $ drop 1 $ reverse $
|
|
|
|
drop 1 $ dropWhile (/= '"') s
|
|
|
|
| otherwise = Nothing
|
|
|
|
|
2014-08-15 22:02:17 +00:00
|
|
|
headRequest :: Request -> Request
|
|
|
|
headRequest r = r
|
|
|
|
{ method = methodHead
|
|
|
|
-- remove defaut Accept-Encoding header, to get actual,
|
|
|
|
-- not gzip compressed size.
|
|
|
|
, requestHeaders = (hAcceptEncoding, B.empty) :
|
|
|
|
filter (\(h, _) -> h /= hAcceptEncoding)
|
|
|
|
(requestHeaders r)
|
|
|
|
}
|
|
|
|
|
2018-04-06 19:58:16 +00:00
|
|
|
{- Download a perhaps large file, with auto-resume of incomplete downloads.
|
|
|
|
-
|
|
|
|
- By default, conduit is used for the download, except for file: urls,
|
|
|
|
- which are copied. If the url scheme is not supported by conduit, falls
|
|
|
|
- back to using curl.
|
2018-05-08 20:11:45 +00:00
|
|
|
-
|
|
|
|
- Displays error message on stderr when download failed.
|
2018-04-06 19:58:16 +00:00
|
|
|
-}
|
2018-04-06 21:00:46 +00:00
|
|
|
download :: MeterUpdate -> URLString -> FilePath -> UrlOptions -> IO Bool
|
2018-05-08 20:11:45 +00:00
|
|
|
download meterupdate url file uo =
|
|
|
|
catchJust matchHttpException go showhttpexception
|
|
|
|
`catchNonAsync` showerr
|
2018-04-06 19:58:16 +00:00
|
|
|
where
|
|
|
|
go = case parseURIRelaxed url of
|
2018-04-06 21:00:46 +00:00
|
|
|
Just u -> case (urlDownloader uo, parseUrlConduit (show u)) of
|
|
|
|
(DownloadWithConduit, Just req) -> catchJust
|
2018-04-06 19:58:16 +00:00
|
|
|
-- When http redirects to a protocol which
|
|
|
|
-- conduit does not support, it will throw
|
|
|
|
-- a StatusCodeException with found302.
|
|
|
|
(matchStatusCodeException (== found302))
|
|
|
|
(downloadconduit req)
|
|
|
|
(const downloadcurl)
|
2018-04-06 21:00:46 +00:00
|
|
|
_
|
2018-04-06 19:58:16 +00:00
|
|
|
| uriScheme u == "file:" -> do
|
|
|
|
let src = unEscapeString (uriPath u)
|
|
|
|
withMeteredFile src meterupdate $
|
|
|
|
L.writeFile file
|
|
|
|
return True
|
2018-04-07 01:17:36 +00:00
|
|
|
| otherwise -> downloadcurl
|
2018-04-06 19:58:16 +00:00
|
|
|
Nothing -> return False
|
|
|
|
|
|
|
|
downloadconduit req = catchMaybeIO (getFileSize file) >>= \case
|
|
|
|
Nothing -> runResourceT $ do
|
2018-05-21 19:10:25 +00:00
|
|
|
resp <- http req' (httpManager uo)
|
2018-04-06 19:58:16 +00:00
|
|
|
if responseStatus resp == ok200
|
|
|
|
then store zeroBytesProcessed WriteMode resp
|
2018-05-08 20:11:45 +00:00
|
|
|
else showrespfailure resp
|
2018-05-21 19:10:25 +00:00
|
|
|
Just sz -> resumeconduit req' sz
|
|
|
|
where
|
|
|
|
-- Override http-client's default decompression of gzip
|
|
|
|
-- compressed files. We want the unmodified file content.
|
|
|
|
req' = req
|
|
|
|
{ requestHeaders = (hAcceptEncoding, "identity") :
|
|
|
|
filter ((/= hAcceptEncoding) . fst)
|
|
|
|
(requestHeaders req)
|
|
|
|
, decompress = const False
|
|
|
|
}
|
2018-04-06 19:58:16 +00:00
|
|
|
|
|
|
|
alreadydownloaded sz s h = s == requestedRangeNotSatisfiable416
|
|
|
|
&& case lookup hContentRange h of
|
|
|
|
-- This could be improved by fixing
|
|
|
|
-- https://github.com/aristidb/http-types/issues/87
|
|
|
|
Just crh -> crh == B8.fromString ("bytes */" ++ show sz)
|
|
|
|
Nothing -> False
|
|
|
|
|
|
|
|
-- Resume download from where a previous download was interrupted,
|
|
|
|
-- when supported by the http server. The server may also opt to
|
|
|
|
-- send the whole file rather than resuming.
|
|
|
|
resumeconduit req sz = catchJust
|
|
|
|
(matchStatusCodeHeadersException (alreadydownloaded sz))
|
|
|
|
dl
|
|
|
|
(const $ return True)
|
|
|
|
where
|
|
|
|
dl = runResourceT $ do
|
|
|
|
let req' = req { requestHeaders = resumeFromHeader sz : requestHeaders req }
|
|
|
|
resp <- http req' (httpManager uo)
|
|
|
|
if responseStatus resp == partialContent206
|
|
|
|
then store (BytesProcessed sz) AppendMode resp
|
|
|
|
else if responseStatus resp == ok200
|
|
|
|
then store zeroBytesProcessed WriteMode resp
|
2018-05-08 20:11:45 +00:00
|
|
|
else showrespfailure resp
|
|
|
|
|
|
|
|
showrespfailure resp = liftIO $ do
|
|
|
|
hPutStrLn stderr $ B8.toString $
|
|
|
|
statusMessage $ responseStatus resp
|
|
|
|
hFlush stderr
|
|
|
|
return False
|
|
|
|
showhttpexception he = do
|
|
|
|
#if MIN_VERSION_http_client(0,5,0)
|
|
|
|
let msg = case he of
|
|
|
|
HttpExceptionRequest _ (StatusCodeException _ msgb) ->
|
|
|
|
B8.toString msgb
|
|
|
|
HttpExceptionRequest _ other -> show other
|
|
|
|
_ -> show he
|
|
|
|
#else
|
|
|
|
let msg = case he of
|
2018-05-10 04:22:23 +00:00
|
|
|
StatusCodeException status _ _ ->
|
|
|
|
B8.toString (statusMessage status)
|
2018-05-08 20:11:45 +00:00
|
|
|
_ -> show he
|
|
|
|
#endif
|
|
|
|
hPutStrLn stderr $ "download failed: " ++ msg
|
|
|
|
hFlush stderr
|
|
|
|
return False
|
|
|
|
showerr e = do
|
|
|
|
hPutStrLn stderr (show e)
|
|
|
|
hFlush stderr
|
|
|
|
return False
|
2018-04-06 19:58:16 +00:00
|
|
|
|
|
|
|
store initialp mode resp = do
|
|
|
|
sinkResponseFile meterupdate initialp file mode resp
|
|
|
|
return True
|
|
|
|
|
|
|
|
downloadcurl = do
|
|
|
|
-- curl does not create destination file
|
|
|
|
-- if the url happens to be empty, so pre-create.
|
|
|
|
unlessM (doesFileExist file) $
|
|
|
|
writeFile file ""
|
2018-04-06 21:00:46 +00:00
|
|
|
let ps = curlParams uo
|
2018-04-06 19:58:16 +00:00
|
|
|
[ Param "-sS"
|
|
|
|
, Param "-f"
|
|
|
|
, Param "-L"
|
|
|
|
, Param "-C", Param "-"
|
|
|
|
]
|
2018-04-06 21:00:46 +00:00
|
|
|
boolSystem "curl" (ps ++ [Param "-o", File file, File url])
|
2018-04-06 19:58:16 +00:00
|
|
|
|
|
|
|
{- Sinks a Response's body to a file. The file can either be opened in
|
|
|
|
- WriteMode or AppendMode. Updates the meter as data is received.
|
|
|
|
-
|
|
|
|
- Note that the responseStatus is not checked by this function.
|
|
|
|
-}
|
2018-04-25 01:23:40 +00:00
|
|
|
sinkResponseFile
|
|
|
|
:: MonadResource m
|
|
|
|
=> MeterUpdate
|
|
|
|
-> BytesProcessed
|
|
|
|
-> FilePath
|
|
|
|
-> IOMode
|
|
|
|
#if MIN_VERSION_http_conduit(2,3,0)
|
|
|
|
-> Response (ConduitM () B8.ByteString m ())
|
|
|
|
#else
|
|
|
|
-> Response (ResumableSource m B8.ByteString)
|
|
|
|
#endif
|
|
|
|
-> m ()
|
2018-04-06 19:58:16 +00:00
|
|
|
sinkResponseFile meterupdate initialp file mode resp = do
|
|
|
|
(fr, fh) <- allocate (openBinaryFile file mode) hClose
|
2018-04-25 01:23:40 +00:00
|
|
|
#if MIN_VERSION_http_conduit(2,3,0)
|
2018-04-22 17:14:55 +00:00
|
|
|
runConduit $ responseBody resp .| go initialp fh
|
2018-04-25 01:23:40 +00:00
|
|
|
#else
|
|
|
|
responseBody resp $$+- go initialp fh
|
|
|
|
#endif
|
2018-04-06 19:58:16 +00:00
|
|
|
release fr
|
|
|
|
where
|
|
|
|
go sofar fh = await >>= \case
|
|
|
|
Nothing -> return ()
|
|
|
|
Just bs -> do
|
|
|
|
let sofar' = addBytesProcessed sofar (B.length bs)
|
|
|
|
liftIO $ do
|
|
|
|
void $ meterupdate sofar'
|
|
|
|
B.hPut fh bs
|
|
|
|
go sofar' fh
|
|
|
|
|
2017-12-06 17:16:06 +00:00
|
|
|
{- Downloads at least the specified number of bytes from an url. -}
|
|
|
|
downloadPartial :: URLString -> UrlOptions -> Int -> IO (Maybe L.ByteString)
|
|
|
|
downloadPartial url uo n = case parseURIRelaxed url of
|
|
|
|
Nothing -> return Nothing
|
|
|
|
Just u -> go u `catchNonAsync` const (return Nothing)
|
|
|
|
where
|
|
|
|
go u = case parseUrlConduit (show u) of
|
|
|
|
Nothing -> return Nothing
|
|
|
|
Just req -> do
|
|
|
|
let req' = applyRequest uo req
|
2018-04-04 19:15:12 +00:00
|
|
|
withResponse req' (httpManager uo) $ \resp ->
|
2017-12-06 17:16:06 +00:00
|
|
|
if responseStatus resp == ok200
|
|
|
|
then Just <$> brread n [] (responseBody resp)
|
|
|
|
else return Nothing
|
|
|
|
|
|
|
|
-- could use brReadSome here, needs newer http-client dependency
|
|
|
|
brread n' l rb
|
|
|
|
| n' <= 0 = return (L.fromChunks (reverse l))
|
|
|
|
| otherwise = do
|
|
|
|
bs <- brRead rb
|
|
|
|
if B.null bs
|
|
|
|
then return (L.fromChunks (reverse l))
|
|
|
|
else brread (n' - B.length bs) (bs:l) rb
|
|
|
|
|
2013-03-11 03:00:33 +00:00
|
|
|
{- Allows for spaces and other stuff in urls, properly escaping them. -}
|
|
|
|
parseURIRelaxed :: URLString -> Maybe URI
|
2015-06-14 17:54:24 +00:00
|
|
|
parseURIRelaxed s = maybe (parseURIRelaxed' s) Just $
|
|
|
|
parseURI $ escapeURIString isAllowedInURI s
|
|
|
|
|
2017-12-06 17:16:06 +00:00
|
|
|
parseUrlConduit :: URLString -> Maybe Request
|
2017-12-11 16:49:23 +00:00
|
|
|
#if MIN_VERSION_http_client(0,4,30)
|
2017-12-06 17:16:06 +00:00
|
|
|
parseUrlConduit = parseUrlThrow
|
|
|
|
#else
|
|
|
|
parseUrlConduit = parseUrl
|
|
|
|
#endif
|
|
|
|
|
2015-06-14 17:54:24 +00:00
|
|
|
{- Some characters like '[' are allowed in eg, the address of
|
|
|
|
- an uri, but cannot appear unescaped further along in the uri.
|
|
|
|
- This handles that, expensively, by successively escaping each character
|
|
|
|
- from the back of the url until the url parses.
|
|
|
|
-}
|
|
|
|
parseURIRelaxed' :: URLString -> Maybe URI
|
|
|
|
parseURIRelaxed' s = go [] (reverse s)
|
2015-06-14 17:39:44 +00:00
|
|
|
where
|
2015-06-14 17:54:24 +00:00
|
|
|
go back [] = parseURI back
|
|
|
|
go back (c:cs) = case parseURI (escapeURIString isAllowedInURI (reverse (c:cs)) ++ back) of
|
|
|
|
Just u -> Just u
|
|
|
|
Nothing -> go (escapeURIChar escapemore c ++ back) cs
|
|
|
|
|
2015-06-14 17:39:44 +00:00
|
|
|
escapemore '[' = False
|
|
|
|
escapemore ']' = False
|
|
|
|
escapemore c = isAllowedInURI c
|
2014-08-17 19:39:01 +00:00
|
|
|
|
|
|
|
hAcceptEncoding :: CI.CI B.ByteString
|
|
|
|
hAcceptEncoding = "Accept-Encoding"
|
|
|
|
|
2015-01-22 18:52:52 +00:00
|
|
|
hContentDisposition :: CI.CI B.ByteString
|
|
|
|
hContentDisposition = "Content-Disposition"
|
|
|
|
|
2018-04-06 19:58:16 +00:00
|
|
|
hContentRange :: CI.CI B.ByteString
|
|
|
|
hContentRange = "Content-Range"
|
|
|
|
|
|
|
|
resumeFromHeader :: FileSize -> Header
|
|
|
|
resumeFromHeader sz = (hRange, renderByteRanges [ByteRangeFrom sz])
|
|
|
|
|
2016-07-12 20:30:36 +00:00
|
|
|
{- Use with eg:
|
|
|
|
-
|
|
|
|
- > catchJust (matchStatusCodeException (== notFound404))
|
|
|
|
-}
|
2016-12-10 12:24:27 +00:00
|
|
|
matchStatusCodeException :: (Status -> Bool) -> HttpException -> Maybe HttpException
|
2018-04-06 19:58:16 +00:00
|
|
|
matchStatusCodeException want = matchStatusCodeHeadersException (\s _h -> want s)
|
|
|
|
|
|
|
|
#if MIN_VERSION_http_client(0,5,0)
|
|
|
|
matchStatusCodeHeadersException :: (Status -> ResponseHeaders -> Bool) -> HttpException -> Maybe HttpException
|
|
|
|
matchStatusCodeHeadersException want e@(HttpExceptionRequest _ (StatusCodeException r _))
|
|
|
|
| want (responseStatus r) (responseHeaders r) = Just e
|
2016-12-10 12:24:27 +00:00
|
|
|
| otherwise = Nothing
|
2018-04-06 19:58:16 +00:00
|
|
|
matchStatusCodeHeadersException _ _ = Nothing
|
2016-12-10 12:24:27 +00:00
|
|
|
#else
|
2018-04-06 19:58:16 +00:00
|
|
|
matchStatusCodeHeadersException :: (Status -> ResponseHeaders -> Bool) -> HttpException -> Maybe HttpException
|
|
|
|
matchStatusCodeHeadersException want e@(StatusCodeException s r _)
|
|
|
|
| want s r = Just e
|
2016-07-12 20:30:36 +00:00
|
|
|
| otherwise = Nothing
|
2018-04-09 17:04:23 +00:00
|
|
|
matchStatusCodeHeadersException _ _ = Nothing
|
2016-12-10 12:24:27 +00:00
|
|
|
#endif
|
2017-09-12 19:13:42 +00:00
|
|
|
|
2018-05-08 20:11:45 +00:00
|
|
|
{- Use with eg:
|
|
|
|
-
|
|
|
|
- > catchJust matchHttpException
|
|
|
|
-}
|
|
|
|
matchHttpException :: HttpException -> Maybe HttpException
|
|
|
|
matchHttpException = Just
|
|
|
|
|
2017-09-13 19:35:42 +00:00
|
|
|
#if MIN_VERSION_http_client(0,5,0)
|
2017-09-12 19:13:42 +00:00
|
|
|
matchHttpExceptionContent :: (HttpExceptionContent -> Bool) -> HttpException -> Maybe HttpException
|
|
|
|
matchHttpExceptionContent want e@(HttpExceptionRequest _ hec)
|
|
|
|
| want hec = Just e
|
|
|
|
| otherwise = Nothing
|
|
|
|
matchHttpExceptionContent _ _ = Nothing
|
2017-09-13 19:35:42 +00:00
|
|
|
#else
|
|
|
|
matchHttpExceptionContent :: (HttpException -> Bool) -> HttpException -> Maybe HttpException
|
|
|
|
matchHttpExceptionContent want e
|
|
|
|
| want e = Just e
|
|
|
|
| otherwise = Nothing
|
|
|
|
#endif
|