215 lines
7.0 KiB
Haskell
215 lines
7.0 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
{-# LANGUAGE TemplateHaskell #-}
|
|
{-# LANGUAGE TypeApplications #-}
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
|
|
|
|
module GHCup.Download.Internal where
|
|
|
|
|
|
import GHCup.Errors
|
|
import GHCup.Types.Optics
|
|
import GHCup.Types.JSON ( )
|
|
import GHCup.Utils.File
|
|
import GHCup.Utils.Prelude
|
|
|
|
import Control.Applicative
|
|
import Control.Exception.Safe
|
|
import Control.Monad
|
|
import Control.Monad.Reader
|
|
import Data.ByteString ( ByteString )
|
|
import Data.ByteString.Builder
|
|
import Data.CaseInsensitive ( CI )
|
|
import Data.IORef
|
|
import Data.Maybe
|
|
import Data.Text.Read
|
|
import HPath
|
|
import HPath.IO as HIO
|
|
import Haskus.Utils.Variant.Excepts
|
|
import Network.HTTP.Client
|
|
import Network.HTTP.Client.OpenSSL
|
|
import Network.HTTP.Types.Status
|
|
import Network.HTTP.Types.Header
|
|
import Optics
|
|
import Prelude hiding ( abs
|
|
, readFile
|
|
, writeFile
|
|
)
|
|
import "unix" System.Posix.IO.ByteString
|
|
hiding ( fdWrite )
|
|
import "unix-bytestring" System.Posix.IO.ByteString
|
|
( fdWrite )
|
|
import System.ProgressBar
|
|
import URI.ByteString
|
|
|
|
import qualified Data.ByteString as BS
|
|
import qualified Data.ByteString.Lazy as L
|
|
import qualified Data.Map.Strict as M
|
|
import qualified Data.Text as T
|
|
import qualified OpenSSL.Session as SSL
|
|
|
|
|
|
|
|
|
|
|
|
----------------------------
|
|
--[ Low-level (non-curl) ]--
|
|
----------------------------
|
|
|
|
|
|
-- | Load the result of this download into memory at once.
|
|
downloadBS' :: (MonadThrow m, MonadIO m)
|
|
=> URI
|
|
-> Excepts
|
|
'[HTTPStatusError]
|
|
m
|
|
(L.ByteString)
|
|
downloadBS' uri' = do
|
|
bref <- liftIO $ newIORef (mempty :: Builder)
|
|
-- TODO: performance
|
|
let stepper bs = modifyIORef bref (<> byteString bs)
|
|
downloadInternal False
|
|
(T.unpack . decUTF8Safe . serializeURIRef' $ uri')
|
|
stepper
|
|
liftIO (readIORef bref <&> toLazyByteString)
|
|
|
|
|
|
downloadToFile :: (MonadMask m, MonadIO m)
|
|
=> URI
|
|
-> Path Abs -- ^ destination file to create and write to
|
|
-> Excepts '[DownloadFailed] m ()
|
|
downloadToFile uri' destFile = do
|
|
fd <- liftIO $ createRegularFileFd newFilePerms destFile
|
|
let stepper = fdWrite fd
|
|
flip finally (liftIO $ closeFd fd)
|
|
$ reThrowAll DownloadFailed
|
|
$ downloadInternal True
|
|
(T.unpack . decUTF8Safe . serializeURIRef' $ uri')
|
|
stepper
|
|
|
|
|
|
downloadInternal :: (MonadThrow m, MonadIO m)
|
|
=> Bool -- ^ whether to show a progress bar
|
|
-> String
|
|
-> (ByteString -> IO a) -- ^ the consuming step function
|
|
-> Excepts
|
|
'[HTTPStatusError]
|
|
m
|
|
()
|
|
downloadInternal progressBar uri' consumer = lEM $ liftIO $ withConnection' action
|
|
where
|
|
action :: (MonadThrow m, MonadIO m) => Manager -> m (Either HTTPStatusError ())
|
|
action m = do
|
|
request <- parseRequest ("GET " <> uri')
|
|
liftIO $ withResponse
|
|
request
|
|
m
|
|
(\r -> do
|
|
let scode = statusCode . responseStatus $ r
|
|
if
|
|
| scode >= 200 && scode < 300 ->
|
|
let headers = M.fromList . responseHeaders $ r
|
|
in fmap Right $ liftIO $ downloadStream (responseBody r) headers
|
|
| otherwise -> pure $ Left $ HTTPStatusError scode
|
|
)
|
|
|
|
downloadStream :: BodyReader -> M.Map HeaderName ByteString -> IO ()
|
|
downloadStream br headers = do
|
|
let size = case M.lookup "Content-Length" headers of
|
|
Just x' -> case decimal $ decUTF8Safe x' of
|
|
Left _ -> 0
|
|
Right (r', _) -> r'
|
|
Nothing -> 0
|
|
|
|
mpb <- if progressBar
|
|
then Just <$> (liftIO $ newProgressBar defStyle 10 (Progress 0 size ()))
|
|
else pure Nothing
|
|
|
|
loop mpb
|
|
|
|
where
|
|
loop mpb = do
|
|
bs <- brRead br
|
|
if BS.length bs == 0 then pure () else do
|
|
void $ consumer bs
|
|
forM_ mpb $ \pb -> incProgress pb (BS.length bs)
|
|
loop mpb
|
|
|
|
|
|
getHead :: (MonadCatch m, MonadIO m)
|
|
=> URI
|
|
-> Excepts
|
|
'[HTTPStatusError, UnsupportedScheme]
|
|
m
|
|
(M.Map (CI ByteString) ByteString)
|
|
getHead uri' | scheme == "https" || scheme == "http" = head'
|
|
| otherwise = throwE UnsupportedScheme
|
|
|
|
where
|
|
scheme = view (uriSchemeL' % schemeBSL') uri'
|
|
head' =
|
|
liftE $ headInternal (T.unpack . decUTF8Safe . serializeURIRef' $ uri')
|
|
|
|
|
|
headInternal :: (MonadThrow m, MonadIO m)
|
|
=> String
|
|
-> Excepts
|
|
'[HTTPStatusError]
|
|
m
|
|
(M.Map (CI ByteString) ByteString)
|
|
headInternal uri' = lEM $ liftIO $ withConnection' action
|
|
where
|
|
action :: (MonadThrow m, MonadIO m)
|
|
=> Manager
|
|
-> m (Either HTTPStatusError (M.Map (CI ByteString) ByteString))
|
|
action m = do
|
|
request <- parseRequest ("HEAD " <> uri')
|
|
liftIO $ withResponse
|
|
request
|
|
m
|
|
(\r -> do
|
|
let scode = statusCode . responseStatus $ r
|
|
if
|
|
| scode >= 200 && scode < 300 -> do
|
|
let headers = responseHeaders r
|
|
pure $ Right $ M.fromList $ headers
|
|
| otherwise -> pure $ Left (HTTPStatusError scode)
|
|
)
|
|
|
|
|
|
withConnection' :: (Manager -> IO a) -> IO a
|
|
withConnection' action = do
|
|
mg <- newManager $ opensslManagerSettings baselineContextSSL
|
|
withOpenSSL (action mg)
|
|
|
|
|
|
baselineContextSSL :: IO SSL.SSLContext
|
|
baselineContextSSL = withOpenSSL $ do
|
|
ctx <- SSL.context
|
|
SSL.contextSetDefaultCiphers ctx
|
|
#if defined(darwin_HOST_OS)
|
|
SSL.contextSetVerificationMode ctx SSL.VerifyNone
|
|
#elif defined(mingw32_HOST_OS)
|
|
SSL.contextSetVerificationMode ctx SSL.VerifyNone
|
|
#elif defined(freebsd_HOST_OS)
|
|
SSL.contextSetCAFile ctx "/usr/local/etc/ssl/cert.pem"
|
|
SSL.contextSetVerificationMode ctx $ SSL.VerifyPeer True True Nothing
|
|
#elif defined(openbsd_HOST_OS)
|
|
SSL.contextSetCAFile ctx "/etc/ssl/cert.pem"
|
|
SSL.contextSetVerificationMode ctx $ SSL.VerifyPeer True True Nothing
|
|
#else
|
|
fedora <- doesDirectoryExist [abs|/etc/pki/tls|]
|
|
if fedora
|
|
then do
|
|
SSL.contextSetCAFile ctx "/etc/pki/tls/certs/ca-bundle.crt"
|
|
else do
|
|
SSL.contextSetCADirectory ctx "/etc/ssl/certs"
|
|
SSL.contextSetVerificationMode ctx $ SSL.VerifyPeer True True Nothing
|
|
#endif
|
|
return ctx
|