gitlib-s3-0.2.1: Data/Git/Backend/S3.hs
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE OverloadedStrings #-}
module Data.Git.Backend.S3
( odbS3Backend
, createS3backend
, readRefs, writeRefs
, mirrorRefsFromS3, mirrorRefsToS3 )
where
import Aws
import Aws.Core
import Aws.S3 hiding (bucketName)
import Bindings.Libgit2.Odb
import Bindings.Libgit2.OdbBackend
import Bindings.Libgit2.Oid
import Bindings.Libgit2.Refs
import Bindings.Libgit2.Types
import Control.Applicative
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Data.Attempt
import Data.Binary
import Data.ByteString as B hiding (putStrLn)
import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy.Char8 as BLC
import Data.ByteString.Unsafe
import Data.Conduit
import Data.Conduit.Binary
import Data.Conduit.List hiding (mapM_, foldM, peek,
catMaybes, sequence)
import Data.Git hiding (getObject)
import Data.Git.Backend
import Data.Git.Backend.Trace
import Data.Git.Error
import Data.Git.Oid
import Data.Int (Int64)
import qualified Data.List as L
import Data.Map
import Data.Maybe
import Data.Stringable
import Data.Text as T
import qualified Data.Text.Encoding as E
import qualified Data.Text.Lazy.Encoding as LE
import qualified Data.Yaml as Y
import Foreign.C.String
import Foreign.C.Types
import Foreign.ForeignPtr
import Foreign.Marshal.Alloc
import Foreign.Marshal.Utils
import Foreign.Ptr
import Foreign.StablePtr
import Foreign.Storable
import Network.HTTP.Conduit hiding (Response)
import Prelude hiding (mapM_, catch)
import System.IO.Unsafe
default (Text)
data OdbS3Backend =
OdbS3Backend { odbS3Parent :: C'git_odb_backend
, httpManager :: StablePtr Manager
, bucketName :: StablePtr Text
, objectPrefix :: StablePtr Text
, configuration :: StablePtr Configuration
, s3configuration :: StablePtr (S3Configuration NormalQuery) }
instance Storable OdbS3Backend where
sizeOf _ = sizeOf (undefined :: C'git_odb_backend) +
sizeOf (undefined :: StablePtr Manager) +
sizeOf (undefined :: StablePtr Text) +
sizeOf (undefined :: StablePtr Text) +
sizeOf (undefined :: StablePtr Configuration) +
sizeOf (undefined :: StablePtr (S3Configuration NormalQuery))
alignment _ = alignment (undefined :: Ptr C'git_odb_backend)
peek p = do
v0 <- peekByteOff p 0
let sizev1 = sizeOf (undefined :: C'git_odb_backend)
v1 <- peekByteOff p sizev1
let sizev2 = sizev1 + sizeOf (undefined :: StablePtr Manager)
v2 <- peekByteOff p sizev2
let sizev3 = sizev2 + sizeOf (undefined :: StablePtr Text)
v3 <- peekByteOff p sizev3
let sizev4 = sizev3 + sizeOf (undefined :: StablePtr Text)
v4 <- peekByteOff p sizev4
let sizev5 = sizev4 + sizeOf (undefined :: StablePtr Configuration)
v5 <- peekByteOff p sizev5
return (OdbS3Backend v0 v1 v2 v3 v4 v5)
poke p (OdbS3Backend v0 v1 v2 v3 v4 v5) = do
pokeByteOff p 0 v0
let sizev1 = sizeOf (undefined :: C'git_odb_backend)
pokeByteOff p sizev1 v1
let sizev2 = sizev1 + sizeOf (undefined :: StablePtr Manager)
pokeByteOff p sizev2 v2
let sizev3 = sizev2 + sizeOf (undefined :: StablePtr Text)
pokeByteOff p sizev3 v3
let sizev4 = sizev3 + sizeOf (undefined :: StablePtr Text)
pokeByteOff p sizev4 v4
let sizev5 = sizev4 + sizeOf (undefined :: StablePtr Configuration)
pokeByteOff p sizev5 v5
return ()
odbS3dispatch ::
MonadIO m =>
(Manager -> Text -> Text
-> Configuration -> S3Configuration NormalQuery -> a -> m b)
-> OdbS3Backend -> a -> m b
odbS3dispatch f odbS3 arg = do
manager <- liftIO $ deRefStablePtr (httpManager odbS3)
bucket <- liftIO $ deRefStablePtr (bucketName odbS3)
prefix <- liftIO $ deRefStablePtr (objectPrefix odbS3)
config <- liftIO $ deRefStablePtr (configuration odbS3)
s3config <- liftIO $ deRefStablePtr (s3configuration odbS3)
f manager bucket prefix config s3config arg
testFileS3' :: Manager -> Text -> Text
-> Configuration -> S3Configuration NormalQuery
-> Text
-> ResourceT IO Bool
testFileS3' manager bucket prefix config s3config filepath =
isJust . readResponse <$>
aws config s3config manager
(headObject bucket (T.append prefix filepath))
testFileS3 :: OdbS3Backend -> Text -> ResourceT IO Bool
testFileS3 = odbS3dispatch testFileS3'
getFileS3' :: Manager -> Text -> Text
-> Configuration -> S3Configuration NormalQuery
-> (Text, Maybe (Int,Int))
-> ResourceT IO (ResumableSource (ResourceT IO) ByteString)
getFileS3' manager bucket prefix config s3config (filepath,range) = do
res <- aws config s3config manager
(getObject bucket (T.append prefix filepath))
{ goResponseContentRange = range }
gor <- readResponseIO res
return (responseBody (gorResponse gor))
getFileS3 :: OdbS3Backend -> Text -> Maybe (Int,Int)
-> ResourceT IO (ResumableSource (ResourceT IO) ByteString)
getFileS3 = curry . odbS3dispatch getFileS3'
putFileS3' :: Manager -> Text -> Text
-> Configuration -> S3Configuration NormalQuery
-> (Text, Source (ResourceT IO) ByteString)
-> ResourceT IO BL.ByteString
putFileS3' manager bucket prefix config s3config (filepath,src) = do
lbs <- BL.fromChunks <$> (src $$ consume)
res <- aws config s3config manager
(putObject bucket (T.append prefix filepath)
(RequestBodyLBS lbs))
_ <- readResponseIO res
return lbs
putFileS3 :: OdbS3Backend -> Text -> Source (ResourceT IO) ByteString
-> ResourceT IO BL.ByteString
putFileS3 = curry . odbS3dispatch putFileS3'
type RefMap = Map Text (Either Text Oid)
instance Y.FromJSON Oid where
parseJSON (Y.String v) =
return . Oid . COid $ unsafePerformIO $ do
ptr <- mallocForeignPtr
withCStringable v $ \cstr ->
withForeignPtr ptr $ \ptr' -> do
r <- c'git_oid_fromstr ptr' cstr
when (r < 0) $ throwIO OidCopyFailed
return ptr
coidToJSON :: ForeignPtr C'git_oid -> Y.Value
coidToJSON coid = unsafePerformIO $ withForeignPtr coid $ \oid ->
Y.toJSON <$> oidToStr oid
instance Y.ToJSON Oid where
toJSON (PartialOid (COid coid) _) = throw RefCannotCreateFromPartialOid
toJSON (Oid (COid coid)) = coidToJSON coid
readRefs :: Ptr C'git_odb_backend -> IO (Maybe RefMap)
readRefs be = do
odbS3 <- peek (castPtr be :: Ptr OdbS3Backend)
exists <- catch (runResourceT $ testFileS3 odbS3 "refs.yml")
(\e -> print (e :: IOException) >> throwIO e)
if exists
then do
bytes <- catch (runResourceT $ do
result <- getFileS3 odbS3 "refs.yml" Nothing
result $$+- await)
(\e -> print (e :: IOException) >> throwIO e)
case bytes of
Nothing -> return Nothing
Just bytes' -> return (Y.decode bytes' :: Maybe RefMap)
else return Nothing
writeRefs :: Ptr C'git_odb_backend -> RefMap -> IO ()
writeRefs be refs = do
let payload = Y.encode refs
odbS3 <- peek (castPtr be :: Ptr OdbS3Backend)
void $ runResourceT $
putFileS3 odbS3 "refs.yml" (sourceLbs (BL.fromChunks [payload]))
mirrorRefsFromS3 :: Ptr C'git_odb_backend -> Repository -> IO ()
mirrorRefsFromS3 be repo = do
refs <- readRefs be
case refs of
Nothing -> return ()
Just refs' ->
forM_ (toList refs') $ \(name, ref) ->
withForeignPtr (repositoryPtr repo) $ \repoPtr ->
withCStringable name $ \namePtr -> alloca $ \ptr -> do
r <- case ref of
Left target ->
withCStringable target $ \targetPtr ->
c'git_reference_create_symbolic ptr repoPtr namePtr
targetPtr 1
Right (PartialOid {}) ->
throwIO RefCannotCreateFromPartialOid
Right (Oid (COid coid)) ->
withForeignPtr coid $ \coidPtr ->
c'git_reference_create_oid ptr repoPtr namePtr coidPtr 1
when (r < 0) $ throwIO RepositoryInvalid
mirrorRefsToS3 :: Ptr C'git_odb_backend -> Repository -> IO ()
mirrorRefsToS3 be repo = do
odbS3 <- peek (castPtr be :: Ptr OdbS3Backend)
refs <- catMaybes <$>
mapAllRefs repo (\name -> do ref <- lookupRef repo name
return (go name <$> ref))
writeRefs be (fromList refs)
where go name ref =
case refTarget ref of
RefTargetSymbolic target -> (name, Left target)
RefTargetId oid -> (name, Right oid)
mapPair :: (a -> b) -> (a,a) -> (b,b)
mapPair f (x,y) = (f x, f y)
odbS3BackendReadCallback :: F'git_odb_backend_read_callback
odbS3BackendReadCallback data_p len_p type_p be oid =
catch go (\e -> print (e :: IOException) >> return (-1))
where
go = do
odbS3 <- peek (castPtr be :: Ptr OdbS3Backend)
oidStr <- oidToStr oid
blocks <- runResourceT $ do
result <- getFileS3 odbS3 (T.pack oidStr) Nothing
result $$+- consume
case blocks of
[] -> return (-1)
bs -> do
let hdrLen = sizeOf (undefined :: Int64) * 2
(len,typ) =
mapPair fromIntegral $
(decode (BL.fromChunks [L.head bs]) :: (Int64,Int64))
content <- mallocBytes len
foldM (\offset x -> do
let xOffset = if offset == 0 then hdrLen else 0
innerLen = B.length x - xOffset
unsafeUseAsCString x $ \cstr ->
copyBytes (content `plusPtr` offset)
(cstr `plusPtr` xOffset) innerLen
return (offset + innerLen)) 0 bs
poke len_p (fromIntegral len)
poke type_p (fromIntegral typ)
poke data_p (castPtr content)
return 0
odbS3BackendReadPrefixCallback :: F'git_odb_backend_read_prefix_callback
odbS3BackendReadPrefixCallback out_oid oid_p len_p type_p be oid len =
return 0
odbS3BackendReadHeaderCallback :: F'git_odb_backend_read_header_callback
odbS3BackendReadHeaderCallback len_p type_p be oid =
catch go (\e -> print (e :: IOException) >> return (-1))
where
go = do
let hdrLen = sizeOf (undefined :: Int64) * 2
odbS3 <- peek (castPtr be :: Ptr OdbS3Backend)
oidStr <- oidToStr oid
bytes <- runResourceT $ do
result <- getFileS3 odbS3 (T.pack oidStr) (Just (0,hdrLen - 1))
result $$+- await
case bytes of
Nothing -> return (-1)
Just bs -> do
let (len,typ) = decode (BL.fromChunks [bs]) :: (Int64,Int64)
poke len_p (fromIntegral len)
poke type_p (fromIntegral typ)
return 0
odbS3BackendWriteCallback :: F'git_odb_backend_write_callback
odbS3BackendWriteCallback oid be obj_data len obj_type = do
r <- c'git_odb_hash oid obj_data len obj_type
case r of
0 -> do
oidStr <- oidToStr oid
odbS3 <- peek (castPtr be :: Ptr OdbS3Backend)
let hdr = encode ((fromIntegral len,
fromIntegral obj_type) :: (Int64,Int64))
bytes <- curry unsafePackCStringLen
(castPtr obj_data) (fromIntegral len)
let payload = BL.append hdr (BL.fromChunks [bytes])
catch (go odbS3 oidStr payload >> return 0)
(\e -> print (e :: IOException) >> return (-1))
n -> return n
where
go odbS3 oidStr payload =
runResourceT $ putFileS3 odbS3 (T.pack oidStr) (sourceLbs payload)
odbS3BackendExistsCallback :: F'git_odb_backend_exists_callback
odbS3BackendExistsCallback be oid = do
oidStr <- oidToStr oid
odbS3 <- peek (castPtr be :: Ptr OdbS3Backend)
exists <- catch (runResourceT $ testFileS3 odbS3 (T.pack oidStr))
(\e -> print (e :: IOException) >> return False)
return $ if exists then 1 else 0
odbS3BackendFreeCallback :: F'git_odb_backend_free_callback
odbS3BackendFreeCallback be = do
backend <- peek be
freeHaskellFunPtr (c'git_odb_backend'read backend)
freeHaskellFunPtr (c'git_odb_backend'read_prefix backend)
freeHaskellFunPtr (c'git_odb_backend'read_header backend)
freeHaskellFunPtr (c'git_odb_backend'write backend)
freeHaskellFunPtr (c'git_odb_backend'exists backend)
odbS3 <- peek (castPtr be :: Ptr OdbS3Backend)
freeStablePtr (httpManager odbS3)
freeStablePtr (bucketName odbS3)
freeStablePtr (objectPrefix odbS3)
freeStablePtr (configuration odbS3)
freeStablePtr (s3configuration odbS3)
foreign export ccall "odbS3BackendFreeCallback"
odbS3BackendFreeCallback :: F'git_odb_backend_free_callback
foreign import ccall "&odbS3BackendFreeCallback"
odbS3BackendFreeCallbackPtr :: FunPtr F'git_odb_backend_free_callback
odbS3Backend :: S3Configuration NormalQuery
-> Configuration
-> Manager -> Text -> Text
-> IO (Ptr C'git_odb_backend)
odbS3Backend s3config config manager bucket prefix = do
readFun <- mk'git_odb_backend_read_callback odbS3BackendReadCallback
readPrefixFun <-
mk'git_odb_backend_read_prefix_callback odbS3BackendReadPrefixCallback
readHeaderFun <-
mk'git_odb_backend_read_header_callback odbS3BackendReadHeaderCallback
writeFun <- mk'git_odb_backend_write_callback odbS3BackendWriteCallback
existsFun <- mk'git_odb_backend_exists_callback odbS3BackendExistsCallback
manager' <- newStablePtr manager
bucket' <- newStablePtr bucket
prefix' <- newStablePtr prefix
s3config' <- newStablePtr s3config
config' <- newStablePtr config
castPtr <$> new OdbS3Backend {
odbS3Parent = C'git_odb_backend {
c'git_odb_backend'odb = nullPtr
, c'git_odb_backend'read = readFun
, c'git_odb_backend'read_prefix = readPrefixFun
, c'git_odb_backend'readstream = nullFunPtr
, c'git_odb_backend'read_header = readHeaderFun
, c'git_odb_backend'write = writeFun
, c'git_odb_backend'writestream = nullFunPtr
, c'git_odb_backend'exists = existsFun
, c'git_odb_backend'free = odbS3BackendFreeCallbackPtr }
, httpManager = manager'
, bucketName = bucket'
, objectPrefix = prefix'
, configuration = config'
, s3configuration = s3config' }
createS3backend :: Text -- ^ bucket
-> Text -- ^ prefix
-> Text -- ^ access key
-> Text -- ^ secret key
-> Maybe Manager
-> Maybe Text -- ^ mock address
-> LogLevel
-> Bool -- ^ tracing?
-> Repository
-> IO Repository
createS3backend bucket prefix access secret mmanager mockAddr level tracing repo = do
manager <- maybe (newManager def) return mmanager
odbS3 <-
odbS3Backend
(case mockAddr of
Nothing -> defServiceConfig
Just addr -> (s3 HTTP (E.encodeUtf8 addr) False) {
s3Port = 10001
, s3RequestStyle = PathStyle })
(Configuration Timestamp Credentials {
accessKeyID = E.encodeUtf8 access
, secretAccessKey = E.encodeUtf8 secret }
(defaultLog level))
manager bucket prefix
if tracing
then do backend <- traceBackend odbS3
odbBackendAdd repo backend 100
else odbBackendAdd repo odbS3 100
-- Start by reading in the known refs from S3
mirrorRefsFromS3 odbS3 repo
-- Whenever a ref is written, update the refs in S3
let onWrite = const $ mirrorRefsToS3 odbS3 repo
return repo { repoOnWriteRef = onWrite : repoOnWriteRef repo }
-- S3.hs