cabal-cache-1.2.0.0: src/HaskellWorks/CabalCache/IO/Lazy.hs
module HaskellWorks.CabalCache.IO.Lazy
( readResource,
readFirstAvailableResource,
resourceExists,
writeResource,
createLocalDirectoryIfMissing,
linkOrCopyResource,
readHttpUri,
removePathRecursive,
) where
import Data.Generics.Product.Any (HasAny(the))
import Data.List.NonEmpty (NonEmpty ((:|)))
import Effectful
import Effectful.Resource
import Effectful.Zoo.Amazonka.Data.AwsEnv
import Effectful.Zoo.Amazonka.Data.AwsError
import Effectful.Zoo.Amazonka.Data.AwsLogEntry
import Effectful.Zoo.Core
import Effectful.Zoo.Core.Error.Static
import Effectful.Zoo.Core.Exception
import Effectful.Zoo.DataLog.Dynamic
import Effectful.Zoo.Lazy.Dynamic
import HaskellWorks.CabalCache.AppError (AwsStatusError(..), HttpError(..), statusCodeOf)
import HaskellWorks.CabalCache.Error (CopyFailed(..), InvalidUrl(..), NotFound(..), NotImplemented(..), UnsupportedUri(..))
import HaskellWorks.CabalCache.Location (Location (..))
import HaskellWorks.Prelude
import Lens.Micro
import Network.URI (URI)
import qualified Control.Concurrent as IO
import qualified Data.ByteString.Lazy as LBS
import qualified Data.List.NonEmpty as NEL
import qualified Data.Text as T
import qualified HaskellWorks.CabalCache.AWS.S3 as S3
import qualified HaskellWorks.CabalCache.IO.Console as CIO
import qualified HaskellWorks.CabalCache.URI as URI
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Client.TLS as HTTPS
import qualified System.Directory as IO
import qualified System.FilePath.Posix as FP
import qualified System.IO as IO
import qualified System.IO.Error as IO
{- HLINT ignore "Redundant do" -}
{- HLINT ignore "Reduce duplication" -}
{- HLINT ignore "Redundant bracket" -}
handleHttpError :: ()
=> r <: Error HttpError
=> r <: Error InvalidUrl
=> Eff r a
-> Eff r a
handleHttpError f = catchIO f $ \(e :: HTTP.HttpException) ->
case e of
HTTP.HttpExceptionRequest request content' -> throw $ HttpError request content'
HTTP.InvalidUrlException url' reason' -> throw $ InvalidUrl (tshow url') (tshow reason')
readResource :: ()
=> r <: DataLog AwsLogEntry
=> r <: Error AwsError
=> r <: Error AwsStatusError
=> r <: Error HttpError
=> r <: Error InvalidUrl
=> r <: Error NotFound
=> r <: Error UnsupportedUri
=> r <: IOE
=> r <: Lazy AwsEnv
=> r <: Resource
=> Int
-> Location
-> Eff r LBS.ByteString
readResource maxRetries = \case
LocalFile path -> do
fileExists <- liftIO $ IO.doesFileExist path
if fileExists
then liftIO $ LBS.readFile path
else throw NotFound
Uri uri -> retryS3 maxRetries $ case uri ^. the @"uriScheme" of
"s3:" -> S3.getS3Uri (URI.reslashUri uri)
"http:" -> readHttpUri (URI.reslashUri uri)
"https:" -> readHttpUri (URI.reslashUri uri)
scheme -> throw $ UnsupportedUri uri $ "Unrecognised uri scheme: " <> T.pack scheme
readFirstAvailableResource :: ()
=> r <: DataLog AwsLogEntry
=> r <: Error AwsError
=> r <: Error AwsStatusError
=> r <: Error HttpError
=> r <: Error InvalidUrl
=> r <: Error NotFound
=> r <: Error UnsupportedUri
=> r <: IOE
=> r <: Lazy AwsEnv
=> r <: Resource
=> NonEmpty Location
-> Int
-> Eff r (LBS.ByteString, Location)
readFirstAvailableResource (a:|as) maxRetries = do
(, a) <$> readResource maxRetries a
& do trap @NotFound \e -> do
case NEL.nonEmpty as of
Nothing -> throw e
Just nas -> readFirstAvailableResource nas maxRetries
& do trap @AwsStatusError \e -> do
case NEL.nonEmpty as of
Nothing -> throw e
Just nas -> readFirstAvailableResource nas maxRetries
& do trap @HttpError \e -> do
case NEL.nonEmpty as of
Nothing -> throw e
Just nas -> readFirstAvailableResource nas maxRetries
safePathIsSymbolLink :: ()
=> r <: IOE
=> FilePath
-> Eff r Bool
safePathIsSymbolLink filePath =
catchIO (liftIO $ IO.pathIsSymbolicLink filePath) handler
where handler :: IOError -> Eff r Bool
handler e = if IO.isDoesNotExistError e
then return False
else return True
resourceExists :: ()
=> r <: DataLog AwsLogEntry
=> r <: Error AwsError
=> r <: Error AwsStatusError
=> r <: Error InvalidUrl
=> r <: Error UnsupportedUri
=> r <: IOE
=> r <: Lazy AwsEnv
=> r <: Resource
=> Location
-> Eff r Bool
resourceExists = \case
LocalFile path -> do
fileExists <- liftIO $ IO.doesFileExist path
if fileExists
then return True
else do
symbolicLinkExists <- safePathIsSymbolLink path
if symbolicLinkExists
then do
target <- liftIO $ IO.getSymbolicLinkTarget path
resourceExists (LocalFile target)
else return False
Uri uri -> case uri ^. the @"uriScheme" of
"s3:" -> do
(True <$ S3.headS3Uri (URI.reslashUri uri))
& trap_ @AwsStatusError (pure False)
-- & trap_ @HttpError (pure False)
"http:" -> do
(True <$ headHttpUri (URI.reslashUri uri))
& trap_ @HttpError (pure False)
& trap_ @AwsStatusError (pure False)
_scheme -> return False
writeResource :: ()
=> r <: DataLog AwsLogEntry
=> r <: Error AwsError
=> r <: Error AwsStatusError
=> r <: Error HttpError
=> r <: Error NotImplemented
=> r <: Error UnsupportedUri
=> r <: IOE
=> r <: Lazy AwsEnv
=> r <: Resource
=> Location
-> Int
-> LBS.ByteString
-> Eff r ()
writeResource loc maxRetries lbs = case loc of
LocalFile path -> liftIO (LBS.writeFile path lbs)
Uri uri' -> retryS3 maxRetries $ case uri' ^. the @"uriScheme" of
"s3:" -> S3.putObject (URI.reslashUri uri') lbs
"http:" -> throw $ NotImplemented "HTTP PUT method not supported"
scheme -> throw $ UnsupportedUri uri' $ "Unrecognised uri scheme: " <> T.pack scheme
createLocalDirectoryIfMissing :: MonadIO m => Location -> m ()
createLocalDirectoryIfMissing = \case
LocalFile path -> liftIO $ IO.createDirectoryIfMissing True path
Uri uri -> case uri ^. the @"uriScheme" of
"s3:" -> return ()
"http:" -> return ()
_scheme -> return ()
retryWhen :: ()
=> r <: Error x
=> r <: IOE
=> Show x
=> (x -> Bool)
-> Int
-> Eff r a
-> Eff r a
retryWhen p n f = f
& do trapIn \exception -> do
if n > 0
then do
if p exception
then do
liftIO $ CIO.hPutStrLn IO.stderr $ "WARNING: " <> tshow exception <> " (retrying)"
liftIO $ IO.threadDelay 1000000
retryWhen p (n - 1) f
else throw exception
else throw exception
retryUnless :: forall x r a. ()
=> Show x
=> r <: Error x
=> r <: IOE
=> (x -> Bool)
-> Int
-> Eff r a
-> Eff r a
retryUnless p = retryWhen (not . p)
retryS3 :: ()
=> r <: Error AwsStatusError
=> r <: IOE
=> Int
-> Eff r a
-> Eff r a
retryS3 maxRetries a = do
retryWhen retryPredicate maxRetries a
where retryPredicate :: AwsStatusError -> Bool
retryPredicate e = statusCodeOf e `elem` retryableHTTPStatuses
-- https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
-- https://stackoverflow.com/a/51770411/2976251
-- another note: linode rate limiting returns 503
retryableHTTPStatuses :: [Int]
retryableHTTPStatuses = [408, 409, 425, 426, 502, 503, 504]
linkOrCopyResource :: ()
=> r <: DataLog AwsLogEntry
=> r <: Error AwsError
=> r <: Error AwsStatusError
=> r <: Error CopyFailed
=> r <: Error NotImplemented
=> r <: Error UnsupportedUri
=> r <: IOE
=> r <: Lazy AwsEnv
=> r <: Resource
=> Location
-> Location
-> Eff r ()
linkOrCopyResource source target = case source of
LocalFile sourcePath -> case target of
LocalFile targetPath -> do
liftIO $ IO.createDirectoryIfMissing True (FP.takeDirectory targetPath)
targetPathExists <- liftIO $ IO.doesFileExist targetPath
unless targetPathExists $ liftIO $ IO.createFileLink sourcePath targetPath
Uri _ -> throw $ NotImplemented "Can't copy between different file backends"
Uri sourceUri -> case target of
LocalFile _targetPath -> throw $ NotImplemented "Can't copy between different file backends"
Uri targetUri -> case (sourceUri ^. the @"uriScheme", targetUri ^. the @"uriScheme") of
("s3:", "s3:") -> retryUnless @AwsStatusError ((== 301) . statusCodeOf) 3 (S3.copyS3Uri (URI.reslashUri sourceUri) (URI.reslashUri targetUri))
("http:", "http:") -> throw $ NotImplemented "Link and copy unsupported for http backend"
(sourceScheme, targetScheme) -> throw $ NotImplemented $ "Unsupported backend combination: " <> T.pack sourceScheme <> " to " <> T.pack targetScheme
readHttpUri :: ()
=> r <: Error HttpError
=> r <: Error InvalidUrl
=> r <: IOE
=> URI
-> Eff r LBS.ByteString
readHttpUri httpUri = handleHttpError do
manager <- liftIO $ HTTP.newManager HTTPS.tlsManagerSettings
request <- liftIO $ HTTP.parseUrlThrow (T.unpack ("GET " <> tshow (URI.reslashUri httpUri)))
response <- liftIO $ HTTP.httpLbs request manager
return $ HTTP.responseBody response
headHttpUri :: ()
=> r <: Error HttpError
=> r <: Error InvalidUrl
=> r <: IOE
=> URI
-> Eff r LBS.ByteString
headHttpUri httpUri = handleHttpError do
manager <- liftIO $ HTTP.newManager HTTP.defaultManagerSettings
request <- liftIO $ HTTP.parseUrlThrow (T.unpack ("HEAD " <> tshow (URI.reslashUri httpUri)))
response <- liftIO $ HTTP.httpLbs request manager
return $ HTTP.responseBody response
removePathRecursive :: ()
=> r <: IOE
=> [Char]
-> Eff r ()
removePathRecursive pkgStorePath =
liftIO (IO.removeDirectoryRecursive pkgStorePath)