2019-07-31 16:06:56 +00:00
|
|
|
{-# LANGUAGE DeriveGeneric, FlexibleInstances, FlexibleContexts #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2019-07-29 19:47:17 +00:00
|
|
|
|
|
|
|
import Data.Aeson
|
|
|
|
import Data.Aeson.Types
|
|
|
|
import GHC.Generics
|
|
|
|
import qualified Data.Map as M
|
|
|
|
import qualified Data.Text as T
|
2019-07-31 16:27:27 +00:00
|
|
|
import qualified Data.Text.Encoding as E
|
2019-07-31 16:06:56 +00:00
|
|
|
import qualified Data.ByteString.Lazy as L
|
2019-07-31 16:27:27 +00:00
|
|
|
import qualified Data.CaseInsensitive as CI
|
2019-07-31 16:06:56 +00:00
|
|
|
import Network.HTTP.Client
|
|
|
|
|
2019-07-29 19:47:17 +00:00
|
|
|
data TransferRequest = TransferRequest
|
|
|
|
{ req_operation :: TransferRequestOperation
|
|
|
|
, req_transfers :: [TransferAdapter]
|
|
|
|
, req_ref :: Maybe GitRef
|
|
|
|
, req_objects :: [TransferRequestObject]
|
|
|
|
}
|
|
|
|
deriving (Generic, Show)
|
|
|
|
|
|
|
|
instance ToJSON TransferRequest where
|
|
|
|
toJSON = genericToJSON transferRequestOptions
|
|
|
|
toEncoding = genericToEncoding transferRequestOptions
|
|
|
|
|
|
|
|
instance FromJSON TransferRequest where
|
|
|
|
parseJSON = genericParseJSON transferRequestOptions
|
|
|
|
|
|
|
|
transferRequestOptions :: Options
|
|
|
|
transferRequestOptions = nonNullOptions
|
|
|
|
-- remove "req_"
|
|
|
|
{ fieldLabelModifier = drop 4 }
|
|
|
|
|
|
|
|
data TransferRequestObject = TransferRequestObject
|
|
|
|
{ oid :: SHA256
|
|
|
|
, size :: Integer
|
|
|
|
}
|
|
|
|
deriving (Generic, Show)
|
|
|
|
|
|
|
|
instance ToJSON TransferRequestObject
|
|
|
|
instance FromJSON TransferRequestObject
|
|
|
|
|
|
|
|
data TransferRequestOperation = RequestDownload | RequestUpload
|
|
|
|
deriving (Show)
|
|
|
|
|
|
|
|
instance ToJSON TransferRequestOperation where
|
|
|
|
toJSON RequestDownload = "download"
|
|
|
|
toJSON RequestUpload = "upload"
|
|
|
|
|
|
|
|
instance FromJSON TransferRequestOperation where
|
|
|
|
parseJSON (String "download") = pure RequestDownload
|
|
|
|
parseJSON (String "upload") = pure RequestUpload
|
|
|
|
parseJSON invalid = typeMismatch "TransferRequestOperation" invalid
|
|
|
|
|
|
|
|
data TransferResponse op = TransferResponse
|
2019-07-31 16:06:56 +00:00
|
|
|
{ transfer :: Maybe TransferAdapter
|
2019-07-29 19:47:17 +00:00
|
|
|
, objects :: [TransferResponseOperation op]
|
|
|
|
}
|
|
|
|
deriving (Generic, Show)
|
|
|
|
|
2019-07-31 16:06:56 +00:00
|
|
|
instance IsTransferResponseOperation op => ToJSON (TransferResponse op) where
|
|
|
|
toJSON = genericToJSON nonNullOptions
|
|
|
|
toEncoding = genericToEncoding nonNullOptions
|
|
|
|
|
|
|
|
instance IsTransferResponseOperation op => FromJSON (TransferResponse op)
|
2019-07-29 19:47:17 +00:00
|
|
|
|
|
|
|
data TransferResponseError = TransferResponseError
|
|
|
|
{ message :: T.Text
|
|
|
|
, request_id :: Maybe T.Text
|
|
|
|
, documentation_url :: Maybe Url
|
|
|
|
}
|
|
|
|
deriving (Generic, Show)
|
|
|
|
|
|
|
|
instance ToJSON TransferResponseError where
|
|
|
|
toJSON = genericToJSON nonNullOptions
|
|
|
|
toEncoding = genericToEncoding nonNullOptions
|
|
|
|
|
|
|
|
instance FromJSON TransferResponseError
|
|
|
|
|
|
|
|
data TransferAdapter = Basic
|
|
|
|
deriving (Show)
|
|
|
|
|
|
|
|
instance ToJSON TransferAdapter where
|
|
|
|
toJSON Basic = "basic"
|
|
|
|
|
|
|
|
instance FromJSON TransferAdapter where
|
|
|
|
parseJSON (String "basic") = pure Basic
|
|
|
|
parseJSON invalid = typeMismatch "basic" invalid
|
|
|
|
|
|
|
|
data TransferResponseOperation op = TransferResponseOperation
|
|
|
|
{ resp_oid :: SHA256
|
|
|
|
, resp_size :: Integer
|
2019-07-31 16:06:56 +00:00
|
|
|
, resp_authenticated :: Maybe Bool
|
2019-07-29 19:47:17 +00:00
|
|
|
, resp_actions :: op
|
|
|
|
}
|
|
|
|
deriving (Generic, Show)
|
|
|
|
|
|
|
|
instance ToJSON op => ToJSON (TransferResponseOperation op) where
|
|
|
|
toJSON = genericToJSON transferResponseOperationOptions
|
|
|
|
toEncoding = genericToEncoding transferResponseOperationOptions
|
|
|
|
|
|
|
|
instance FromJSON op => FromJSON (TransferResponseOperation op) where
|
|
|
|
parseJSON = genericParseJSON transferResponseOperationOptions
|
|
|
|
|
|
|
|
transferResponseOperationOptions :: Options
|
2019-07-31 16:06:56 +00:00
|
|
|
transferResponseOperationOptions = nonNullOptions
|
2019-07-29 19:47:17 +00:00
|
|
|
-- remove "resp_"
|
|
|
|
{ fieldLabelModifier = drop 5 }
|
|
|
|
|
2019-07-31 16:06:56 +00:00
|
|
|
-- | Class of types that can be responses to a transfer request,
|
|
|
|
-- that contain an operation to use to make the transfer.
|
|
|
|
class (FromJSON op, ToJSON op) => IsTransferResponseOperation op
|
|
|
|
|
2019-07-29 19:47:17 +00:00
|
|
|
data DownloadOperation = DownloadOperation
|
|
|
|
{ download :: OperationParams }
|
|
|
|
deriving (Generic, Show)
|
|
|
|
|
2019-07-31 16:06:56 +00:00
|
|
|
instance IsTransferResponseOperation DownloadOperation
|
2019-07-29 19:47:17 +00:00
|
|
|
instance ToJSON DownloadOperation
|
|
|
|
instance FromJSON DownloadOperation
|
|
|
|
|
|
|
|
data UploadOperation = UploadOperation
|
|
|
|
{ upload :: OperationParams }
|
|
|
|
deriving (Generic, Show)
|
|
|
|
|
2019-07-31 16:06:56 +00:00
|
|
|
instance IsTransferResponseOperation UploadOperation
|
2019-07-29 19:47:17 +00:00
|
|
|
instance FromJSON UploadOperation
|
|
|
|
instance ToJSON UploadOperation
|
|
|
|
|
|
|
|
data OperationParams = OperationParams
|
|
|
|
{ href :: Url
|
2019-07-31 16:06:56 +00:00
|
|
|
, header :: Maybe (M.Map HTTPHeader HTTPHeaderValue)
|
2019-07-29 19:47:17 +00:00
|
|
|
, expires_in :: Maybe NumSeconds
|
|
|
|
, expires_at :: Maybe T.Text
|
|
|
|
}
|
|
|
|
deriving (Generic, Show)
|
|
|
|
|
|
|
|
instance ToJSON OperationParams where
|
|
|
|
toJSON = genericToJSON nonNullOptions
|
|
|
|
toEncoding = genericToEncoding nonNullOptions
|
|
|
|
|
|
|
|
instance FromJSON OperationParams
|
|
|
|
|
|
|
|
data GitRef = GitRef
|
|
|
|
{ name :: T.Text }
|
|
|
|
deriving (Generic, Show)
|
|
|
|
|
|
|
|
instance FromJSON GitRef
|
|
|
|
instance ToJSON GitRef
|
|
|
|
|
|
|
|
type SHA256 = T.Text
|
|
|
|
|
2019-07-31 17:22:33 +00:00
|
|
|
-- | Adds necessary headers to a Request and makes it post the
|
|
|
|
-- specified TransferRequest.
|
|
|
|
--
|
|
|
|
-- The input Request's url should be the discovered LFS endpoint.
|
|
|
|
-- Since this uses the LFS batch API, it adds /objects/batch to the end of
|
|
|
|
-- that url.
|
|
|
|
transferRequest :: Request -> TransferRequest -> Request
|
|
|
|
transferRequest r tr = r
|
|
|
|
{ path = path r <> "/objects/batch"
|
|
|
|
, requestHeaders =
|
|
|
|
[ ("Accept", lfsjson)
|
|
|
|
, ("Content-Type", lfsjson)
|
|
|
|
]
|
|
|
|
, method = "POST"
|
|
|
|
, requestBody = RequestBodyLBS (encode tr)
|
|
|
|
}
|
|
|
|
where
|
|
|
|
lfsjson = "application/vnd.git-lfs+json"
|
|
|
|
|
|
|
|
type ParsedTransferResponse op =
|
|
|
|
Either (Either String TransferResponseError) (TransferResponse op)
|
|
|
|
|
|
|
|
-- | Parse the body of a response to a transfer request.
|
|
|
|
parseResponseBody
|
|
|
|
:: IsTransferResponseOperation op
|
|
|
|
=> L.ByteString
|
|
|
|
-> ParsedTransferResponse op
|
|
|
|
parseResponseBody resp = case eitherDecode resp of
|
|
|
|
-- If unable to decode as a TransferResponse, try to decode
|
|
|
|
-- as a TransferResponseError instead, in case the LFS server
|
|
|
|
-- sent an error message.
|
|
|
|
Left err -> case eitherDecode resp of
|
|
|
|
Right responseerror -> Left (Right responseerror)
|
|
|
|
Left _ -> Left $ Left err
|
|
|
|
Right tr -> Right tr
|
|
|
|
|
|
|
|
-- | Builds http requests that can be used to download the objects that
|
|
|
|
-- were requested using a TransferRequest.
|
|
|
|
downloadRequests :: TransferResponse DownloadOperation -> ([(TransferResponseOperation DownloadOperation, Maybe Request)])
|
|
|
|
downloadRequests = transferRequests download
|
|
|
|
|
|
|
|
-- | Builds http requests that can be used to upload objects.
|
|
|
|
--
|
|
|
|
-- When the server already has an object, no request will be generated.
|
|
|
|
--
|
|
|
|
-- The requestBody is not set here. When making a request,
|
|
|
|
-- the content of the object needs to be provided as the body.
|
|
|
|
uploadRequests :: TransferResponse UploadOperation -> ([(TransferResponseOperation UploadOperation, Maybe Request)])
|
|
|
|
uploadRequests = transferRequests upload
|
|
|
|
|
|
|
|
transferRequests :: (op -> OperationParams) -> TransferResponse op -> ([(TransferResponseOperation op, Maybe Request)])
|
|
|
|
transferRequests getps = map mkreq . objects
|
|
|
|
where
|
|
|
|
mkreq op = (op, mkreq' (getps (resp_actions op)))
|
|
|
|
mkreq' ps = do
|
|
|
|
r <- parseRequest (T.unpack (href ps))
|
|
|
|
let headers = map convheader $ maybe [] M.toList (header ps)
|
|
|
|
return $ r { requestHeaders = headers }
|
|
|
|
convheader (k, v) = (CI.mk (E.encodeUtf8 k), E.encodeUtf8 v)
|
|
|
|
|
2019-07-29 19:47:17 +00:00
|
|
|
type Url = T.Text
|
|
|
|
|
|
|
|
type NumSeconds = Integer
|
|
|
|
|
|
|
|
type HTTPHeader = T.Text
|
|
|
|
|
|
|
|
type HTTPHeaderValue = T.Text
|
|
|
|
|
|
|
|
-- Prevent Nothing from serializing to null.
|
|
|
|
nonNullOptions :: Options
|
|
|
|
nonNullOptions = defaultOptions { omitNothingFields = True }
|