11{-# LANGUAGE CPP #-}
22{-# LANGUAGE DeriveLift #-}
3+ {-# LANGUAGE LambdaCase #-}
34{-# LANGUAGE OverloadedStrings #-}
45
5- -- |
6+ -- | Types and functions for a client for Casa (Content-addressable Storage
7+ -- Archive).
68
79module Casa.Client
810 ( blobsSource
9- , SourceConfig (.. )
11+ , SourceConfig (.. )
1012 , blobsSink
1113 , CasaRepoPrefix
1214 , parseCasaRepoPrefix
1315 , thParserCasaRepo
14- , PushException (.. )
15- , PullException (.. )
16+ , PushException (.. )
17+ , PullException (.. )
1618 ) where
1719
1820import Casa.Types
19- import Control.Monad
20- import Control.Monad.Catch
21- import Control.Monad.IO.Class
21+ ( BlobKey (.. ), blobKeyBinaryParser , blobKeyToBuilder )
22+ import Control.Monad ( (>=>) , unless )
23+ import Control.Monad.Catch ( Exception , MonadThrow , throwM )
24+ import Control.Monad.IO.Class ( MonadIO )
2225import Control.Monad.IO.Unlift
23- import Control.Monad.Trans.Resource
26+ ( MonadUnliftIO , UnliftIO (.. ), askUnliftIO )
27+ import Control.Monad.Trans.Resource ( MonadResource )
2428import qualified Crypto.Hash as Crypto
25- import Data.Aeson
29+ import Data.Aeson ( FromJSON ( .. ) )
2630import qualified Data.Attoparsec.ByteString as Atto
2731import qualified Data.ByteArray as Mem
28- import Data.ByteString (ByteString )
32+ import Data.ByteString ( ByteString )
2933import qualified Data.ByteString as S
3034import qualified Data.ByteString.Builder as SB
31- import Data.Conduit
32- import Data.Conduit.Attoparsec
33- import Data.Conduit.ByteString.Builder
35+ import Data.Conduit ( ConduitT , (.|) , await , transPipe , yield )
36+ import Data.Conduit.Attoparsec ( ParseError , conduitParserEither )
37+ import Data.Conduit.ByteString.Builder ( builderToByteString )
3438import qualified Data.Conduit.List as CL
35- import Data.HashMap.Strict (HashMap )
39+ import Data.HashMap.Strict ( HashMap )
3640import qualified Data.HashMap.Strict as HM
3741#if !MIN_VERSION_base(4,20,0)
3842import Data.Foldable ( foldl' )
3943#endif
40- import Language.Haskell.TH
41- import Language.Haskell.TH.Lift
44+ import Language.Haskell.TH ( Exp , Q )
45+ import Language.Haskell.TH.Lift ( Lift ( .. ) )
4246import Network.HTTP.Client.Conduit ( requestBodySourceChunked )
4347import Network.HTTP.Simple
44- import Network.HTTP.Types
45- import Network.URI
48+ ( Request , getResponseBody , getResponseStatus
49+ , httpNoBody , httpSource , parseRequest , setRequestBody
50+ , setRequestBodyLBS , setRequestMethod
51+ )
52+ import Network.HTTP.Types ( Status (.. ) )
53+ import Network.URI ( parseURI )
4654
4755-- | An exception from blob consuming/sending.
4856data PullException
4957 = AttoParseError ParseError
5058 | BadHttpStatus Status
5159 | TooManyReturnedKeys Int
5260 deriving Show
61+
5362instance Exception PullException
5463
5564-- | An exception from blob consuming/sending.
5665newtype PushException
5766 = PushBadHttpStatus Status
5867 deriving Show
68+
5969instance Exception PushException
6070
61- -- | The URL prefix for a casa repo.
62- -- Commonly: @https://casa.fpcomplete.com@
71+ -- | The URL prefix for a Casa repository, commonly @https://casa.stackage.org@.
6372-- Parsers will strip out a trailing slash.
6473newtype CasaRepoPrefix =
6574 CasaRepoPrefix String
6675 deriving (Show , Lift )
6776
6877instance FromJSON CasaRepoPrefix where
69- parseJSON j = do
70- s <- parseJSON j
71- either fail pure (parseCasaRepoPrefix s)
78+ parseJSON = parseJSON >=> (either fail pure . parseCasaRepoPrefix)
7279
7380-- | TH compile-time parser.
7481thParserCasaRepo :: String -> Q Exp
@@ -80,7 +87,7 @@ parseCasaRepoPrefix s =
8087 case parseURI s of
8188 Nothing ->
8289 Left
83- " Invalid URI for repo . Should be a valid URI e.g. https://casa.fpcomplete.com "
90+ " Invalid URI for repository . Should be a valid URI e.g. https://casa.stackage.org "
8491 Just {} -> pure (CasaRepoPrefix (stripTrailing s))
8592 where
8693 stripTrailing = reverse . dropWhile (== ' /' ) . reverse
@@ -91,11 +98,13 @@ casaServerVersion = "v1"
9198
9299-- | Build the URL from a repo prefix.
93100casaRepoPushUrl :: CasaRepoPrefix -> String
94- casaRepoPushUrl (CasaRepoPrefix uri) = uri ++ " /" ++ casaServerVersion ++ " /push"
101+ casaRepoPushUrl (CasaRepoPrefix uri) =
102+ uri <> " /" <> casaServerVersion <> " /push"
95103
96104-- | Build the URL from a repo prefix.
97105casaRepoPullUrl :: CasaRepoPrefix -> String
98- casaRepoPullUrl (CasaRepoPrefix uri) = uri ++ " /" ++ casaServerVersion ++ " /pull"
106+ casaRepoPullUrl (CasaRepoPrefix uri) =
107+ uri <> " /" <> casaServerVersion <> " /pull"
99108
100109-- | A sink to push blobs to the server. Throws 'PushException'.
101110blobsSink ::
@@ -113,14 +122,19 @@ blobsSink casaRepoUrl blobs = do
113122 where
114123 makeRequest (UnliftIO runInIO) =
115124 fmap
116- (setRequestBody
117- (requestBodySourceChunked
118- (transPipe runInIO blobs .|
119- CL. map
120- (\ v ->
121- SB. word64BE (fromIntegral (S. length v)) <> SB. byteString v) .|
122- builderToByteString)) .
123- setRequestMethod " POST" )
125+ ( setRequestBody
126+ ( requestBodySourceChunked
127+ ( transPipe runInIO blobs
128+ .| CL. map
129+ ( \ v ->
130+ SB. word64BE (fromIntegral (S. length v))
131+ <> SB. byteString v
132+ )
133+ .| builderToByteString
134+ )
135+ )
136+ . setRequestMethod " POST"
137+ )
124138 (parseRequest (casaRepoPushUrl casaRepoUrl))
125139
126140-- | Configuration for sourcing blobs from the server.
@@ -131,8 +145,8 @@ data SourceConfig =
131145 , sourceConfigBlobs :: ! (HashMap BlobKey Int )
132146 -- ^ The blobs to pull.
133147 , sourceConfigMaxBlobsPerRequest :: ! Int
134- -- ^ Maximum number of blobs per request; we split requests into
135- -- chunks of this number.
148+ -- ^ Maximum number of blobs per request; we split requests into chunks of
149+ -- this number.
136150 }
137151
138152-- | Make a source of blobs from a URL. Throws 'PullException'.
@@ -142,55 +156,51 @@ blobsSource ::
142156 -> ConduitT i (BlobKey , ByteString ) m ()
143157blobsSource sourceConfig = do
144158 skeletonRequest <- makeSkeletonRequest
145- source skeletonRequest (HM. toList (sourceConfigBlobs sourceConfig)) .| conduit .|
146- consumer (HM. size (sourceConfigBlobs sourceConfig))
147- where
148- makeSkeletonRequest =
149- fmap
150- (setRequestMethod " POST" )
151- (parseRequest (casaRepoPullUrl (sourceConfigUrl sourceConfig)))
152- source skeletonRequest blobs =
153- unless
154- (null blobs)
155- (do httpSource
156- filledRequest
157- (\ response ->
158- case getResponseStatus response of
159- Status 200 _ -> getResponseBody response
160- status -> throwM (BadHttpStatus status))
161- source skeletonRequest remainingBlobs)
162- where
163- (filledRequest, remainingBlobs) =
164- setRequestBlobs sourceConfig blobs skeletonRequest
165- conduit =
166- conduitParserEither (blobKeyValueParser (sourceConfigBlobs sourceConfig))
167- consumer remaining = do
168- mkeyValue <- await
169- case mkeyValue of
170- Nothing -> pure ()
171- Just (Left x) -> throwM (AttoParseError x)
172- Just (Right (_position, keyValue)) ->
173- if remaining == 0
174- then throwM
175- (TooManyReturnedKeys
176- (HM. size (sourceConfigBlobs sourceConfig)))
177- else do
178- yield keyValue
179- consumer (remaining - 1 )
159+ source skeletonRequest scBlobsList .| conduit .| consumer scBlobsSize
160+ where
161+ makeSkeletonRequest =
162+ fmap
163+ (setRequestMethod " POST" )
164+ (parseRequest (casaRepoPullUrl (sourceConfigUrl sourceConfig)))
165+ scBlobs = sourceConfigBlobs sourceConfig
166+ scBlobsList = HM. toList scBlobs
167+ scBlobsSize = HM. size scBlobs
168+ source skeletonRequest blobs =
169+ unless (null blobs) $ do
170+ httpSource filledRequest $ \ response ->
171+ case getResponseStatus response of
172+ Status 200 _ -> getResponseBody response
173+ status -> throwM (BadHttpStatus status)
174+ source skeletonRequest remainingBlobs
175+ where
176+ (filledRequest, remainingBlobs) =
177+ setRequestBlobs sourceConfig blobs skeletonRequest
178+ conduit = conduitParserEither (blobKeyValueParser scBlobs)
179+ consumer remaining = await >>= \ case
180+ Nothing -> pure ()
181+ Just (Left x) -> throwM (AttoParseError x)
182+ Just (Right (_position, keyValue)) ->
183+ if remaining == 0
184+ then throwM (TooManyReturnedKeys scBlobsSize)
185+ else do
186+ yield keyValue
187+ consumer (remaining - 1 )
180188
181189-- | Fill the body of the request with max blobs per request.
182190setRequestBlobs ::
183- SourceConfig -> [(BlobKey , Int )] -> Request -> (Request , [(BlobKey , Int )])
191+ SourceConfig -> [(BlobKey , Int )] -> Request -> (Request , [(BlobKey , Int )])
184192setRequestBlobs sourceConfig blobs skeletonRequest = (request, remaining)
185193 where
186194 request =
187195 setRequestBodyLBS
188- (SB. toLazyByteString
189- (foldl'
190- (\ a (k, v) ->
191- a <> (blobKeyToBuilder k <> SB. word64BE (fromIntegral v)))
192- mempty
193- thisBatch))
196+ ( SB. toLazyByteString
197+ ( foldl'
198+ (\ a (k, v) ->
199+ a <> (blobKeyToBuilder k <> SB. word64BE (fromIntegral v)))
200+ mempty
201+ thisBatch
202+ )
203+ )
194204 skeletonRequest
195205 (thisBatch, remaining) =
196206 splitAt (sourceConfigMaxBlobsPerRequest sourceConfig) blobs
@@ -200,12 +210,12 @@ blobKeyValueParser :: HashMap BlobKey Int -> Atto.Parser (BlobKey, ByteString)
200210blobKeyValueParser lengths = do
201211 blobKey <- blobKeyBinaryParser
202212 case HM. lookup blobKey lengths of
203- Nothing -> fail ( " Invalid key: " <> show blobKey)
213+ Nothing -> fail $ " Invalid key: " <> show blobKey
204214 Just len -> do
205215 blob <- Atto. take len
206216 if BlobKey (sha256Hash blob) == blobKey
207217 then pure (blobKey, blob)
208- else fail ( " Content does not match SHA256 hash: " ++ show blobKey)
218+ else fail $ " Content does not match SHA256 hash: " <> show blobKey
209219
210220-- | Hash some raw bytes.
211221sha256Hash :: ByteString -> ByteString
0 commit comments