Basic putObject is working:

- This is single PUT action - so only files up to 5GB.
- Buffers in memory because chunked singature is not yet implemented.
- fPutObject is simply wired to putObject (so does not yet work for
  multipart uploads
This commit is contained in:
Aditya Manthramurthy 2017-01-13 11:09:02 +05:30
parent 9aacd28f43
commit ca3276cd87
7 changed files with 110 additions and 18 deletions

View File

@ -36,6 +36,7 @@ library
, conduit-extra , conduit-extra
, containers , containers
, cryptonite , cryptonite
, cryptonite-conduit
, errors , errors
, filepath , filepath
, http-client , http-client
@ -55,6 +56,7 @@ library
, NoImplicitPrelude , NoImplicitPrelude
, MultiParamTypeClasses , MultiParamTypeClasses
, MultiWayIf , MultiWayIf
, RankNTypes
executable minio-hs-exe executable minio-hs-exe
hs-source-dirs: app hs-source-dirs: app
@ -84,6 +86,7 @@ test-suite minio-hs-test
, conduit-extra , conduit-extra
, containers , containers
, cryptonite , cryptonite
, cryptonite-conduit
, errors , errors
, filepath , filepath
, http-client , http-client
@ -108,6 +111,7 @@ test-suite minio-hs-test
, NoImplicitPrelude , NoImplicitPrelude
, MultiParamTypeClasses , MultiParamTypeClasses
, MultiWayIf , MultiWayIf
, RankNTypes
other-modules: Lib.Prelude other-modules: Lib.Prelude
, Network.Minio , Network.Minio
, Network.Minio.API , Network.Minio.API

View File

@ -1,6 +1,7 @@
module Network.Minio module Network.Minio
( module Exports ( module Exports
, fGetObject , fGetObject
, fPutObject
) where ) where
{- {-
@ -21,11 +22,13 @@ import Network.Minio.Data as
, ConnectInfo(..) , ConnectInfo(..)
) )
import System.FilePath import System.FilePath (FilePath)
import qualified System.IO as IO
import qualified Data.Conduit as C import qualified Data.Conduit as C
import qualified Control.Monad.Trans.Resource as R
import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.Binary as CB
-- import Lib.Prelude import Lib.Prelude
import Network.Minio.Data import Network.Minio.Data
import Network.Minio.S3API import Network.Minio.S3API
@ -34,3 +37,16 @@ fGetObject :: Bucket -> Object -> FilePath -> Minio ()
fGetObject bucket object fp = do fGetObject bucket object fp = do
(_, src) <- getObject bucket object [] [] (_, src) <- getObject bucket object [] []
src C.$$+- CB.sinkFileCautious fp src C.$$+- CB.sinkFileCautious fp
fPutObject :: Bucket -> Object -> FilePath -> Minio ()
fPutObject bucket object fp = do
-- allocate file handle and register cleanup action
(releaseKey, h) <- R.allocate
(IO.openBinaryFile fp IO.ReadMode)
IO.hClose
size <- liftIO $ IO.hFileSize h
putObject bucket object [] 0 (fromIntegral size) h
-- release file handle
R.release releaseKey

View File

@ -14,10 +14,11 @@ import Network.HTTP.Conduit (Response)
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
import Network.HTTP.Types (Method, Header, Query) import Network.HTTP.Types (Method, Header, Query)
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.Conduit as C
import Data.Conduit.Binary (sourceHandleRange)
import Lib.Prelude import Lib.Prelude
import qualified Data.Conduit as C
import Network.Minio.Data import Network.Minio.Data
import Network.Minio.Data.Crypto import Network.Minio.Data.Crypto
@ -35,12 +36,23 @@ import Network.Minio.Sign.V4
-- -- print $ NC.requestBody r -- -- print $ NC.requestBody r
-- NC.httpLbs r mgr -- NC.httpLbs r mgr
payloadBodyWithHash :: (MonadIO m) => RequestInfo
-> m (ByteString, NC.RequestBody)
payloadBodyWithHash ri = case riPayload ri of
EPayload -> return (hashSHA256 "", NC.RequestBodyBS "")
PayloadBS bs -> return (hashSHA256 bs, NC.RequestBodyBS bs)
PayloadH h off size -> do
let offM = return . fromIntegral $ off
sizeM = return . fromIntegral $ size
hash <- hashSHA256FromSource $ sourceHandleRange h offM sizeM
return (hash, NC.requestBodySource (fromIntegral size) $
sourceHandleRange h offM sizeM)
buildRequest :: (MonadIO m, MonadReader MinioConn m) buildRequest :: (MonadIO m, MonadReader MinioConn m)
=> RequestInfo -> m NC.Request => RequestInfo -> m NC.Request
buildRequest ri = do buildRequest ri = do
let pload = maybe "" identity $ riPayload ri (phash, rbody) <- payloadBodyWithHash ri
phash = hashSHA256 pload let newRi = ri {
newRi = ri {
riPayloadHash = phash riPayloadHash = phash
, riHeaders = ("x-amz-content-sha256", phash) : (riHeaders ri) , riHeaders = ("x-amz-content-sha256", phash) : (riHeaders ri)
} }
@ -57,7 +69,7 @@ buildRequest ri = do
, NC.path = getPathFromRI ri , NC.path = getPathFromRI ri
, NC.queryString = HT.renderQuery False $ riQueryParams ri , NC.queryString = HT.renderQuery False $ riQueryParams ri
, NC.requestHeaders = reqHeaders , NC.requestHeaders = reqHeaders
, NC.requestBody = NC.RequestBodyBS pload , NC.requestBody = rbody
} }
isFailureStatus :: Response body -> Bool isFailureStatus :: Response body -> Bool

View File

@ -11,10 +11,11 @@ module Network.Minio.Data
, getRegionFromRI , getRegionFromRI
, Minio , Minio
, MinioErr(..) , MinioErr(..)
, MErrV(..)
, runMinio , runMinio
, defaultConnectInfo , defaultConnectInfo
, connect , connect
, Payload , Payload(..)
, s3Name , s3Name
) where ) where
@ -53,7 +54,12 @@ data BucketInfo = BucketInfo {
, biCreationDate :: UTCTime , biCreationDate :: UTCTime
} deriving (Show, Eq) } deriving (Show, Eq)
type Payload = Maybe ByteString
data Payload = EPayload
| PayloadBS ByteString
| PayloadH Handle
Int64 -- offset
Int64 -- size
data RequestInfo = RequestInfo { data RequestInfo = RequestInfo {
riMethod :: Method riMethod :: Method
@ -76,10 +82,14 @@ getPathFromRI ri = B.concat $ parts
getRegionFromRI :: RequestInfo -> Text getRegionFromRI :: RequestInfo -> Text
getRegionFromRI ri = maybe "us-east-1" identity (riRegion ri) getRegionFromRI ri = maybe "us-east-1" identity (riRegion ri)
data MErrV = MErrVSinglePUTSizeExceeded Int64
deriving (Show)
data MinioErr = MErrMsg ByteString -- generic data MinioErr = MErrMsg ByteString -- generic
| MErrHttp HttpException -- http exceptions | MErrHttp HttpException -- http exceptions
| MErrXml ByteString -- XML parsing/generation errors | MErrXml ByteString -- XML parsing/generation errors
| MErrService ByteString -- error response from service | MErrService ByteString -- error response from service
| MErrValidation MErrV -- client-side validation errors
deriving (Show) deriving (Show)
newtype Minio a = Minio { newtype Minio a = Minio {

View File

@ -1,21 +1,33 @@
module Network.Minio.Data.Crypto module Network.Minio.Data.Crypto
( (
hashSHA256 hashSHA256
, hashSHA256FromSource
, hmacSHA256 , hmacSHA256
, hmacSHA256RawBS , hmacSHA256RawBS
, digestToBS , digestToBS
, digestToBase16 , digestToBase16
) where ) where
import Crypto.Hash (SHA256(..), hashWith) import Crypto.Hash (SHA256(..), hashWith, Digest)
import Crypto.MAC.HMAC (hmac, HMAC) import Crypto.MAC.HMAC (hmac, HMAC)
import Data.ByteArray (ByteArrayAccess, convert) import Data.ByteArray (ByteArrayAccess, convert)
import Data.ByteArray.Encoding (convertToBase, Base(Base16)) import Data.ByteArray.Encoding (convertToBase, Base(Base16))
import qualified Data.Conduit as C
import Crypto.Hash.Conduit (sinkHash)
import Lib.Prelude import Lib.Prelude
hashSHA256 :: ByteString -> ByteString hashSHA256 :: ByteString -> ByteString
hashSHA256 = convertToBase Base16 . hashWith SHA256 hashSHA256 = digestToBase16 . hashWith SHA256
hashSHA256FromSource :: Monad m => C.Producer m ByteString -> m ByteString
hashSHA256FromSource src = do
digest <- src C.$$ sinkSHA256Hash
return $ digestToBase16 digest
where
-- To help with type inference
sinkSHA256Hash :: Monad m => C.Consumer ByteString m (Digest SHA256)
sinkSHA256Hash = sinkHash
hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256 hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
hmacSHA256 message key = hmac key message hmacSHA256 message key = hmac key message

View File

@ -3,6 +3,7 @@ module Network.Minio.S3API
, getLocation , getLocation
, getObject , getObject
, putBucket , putBucket
, putObject
, deleteBucket , deleteBucket
, deleteObject , deleteObject
) where ) where
@ -10,26 +11,29 @@ module Network.Minio.S3API
import qualified Network.HTTP.Types as HT import qualified Network.HTTP.Types as HT
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
import qualified Data.Conduit as C import qualified Data.Conduit as C
-- import Control.Monad.Trans.Resource (MonadResource)
-- import Data.Conduit.Binary (sinkLbs, sourceHandleRange)
-- import qualified Data.ByteString.Lazy as LB
import Lib.Prelude import Lib.Prelude
import Network.Minio.Data import Network.Minio.Data
import Network.Minio.API import Network.Minio.API
import Network.Minio.XmlParser import Network.Minio.XmlParser
import Network.Minio.XmlGenerator import Network.Minio.XmlGenerator
getService :: Minio [BucketInfo] getService :: Minio [BucketInfo]
getService = do getService = do
resp <- executeRequest $ resp <- executeRequest $
requestInfo HT.methodGet Nothing Nothing [] [] Nothing requestInfo HT.methodGet Nothing Nothing [] [] EPayload
parseListBuckets $ NC.responseBody resp parseListBuckets $ NC.responseBody resp
getLocation :: Bucket -> Minio Text getLocation :: Bucket -> Minio Text
getLocation bucket = do getLocation bucket = do
resp <- executeRequest $ resp <- executeRequest $
requestInfo HT.methodGet (Just bucket) Nothing [("location", Nothing)] [] requestInfo HT.methodGet (Just bucket) Nothing [("location", Nothing)] []
Nothing EPayload
parseLocation $ NC.responseBody resp parseLocation $ NC.responseBody resp
getObject :: Bucket -> Object -> HT.Query -> [HT.Header] getObject :: Bucket -> Object -> HT.Query -> [HT.Header]
@ -39,20 +43,36 @@ getObject bucket object queryParams headers = do
return $ (NC.responseHeaders resp, NC.responseBody resp) return $ (NC.responseHeaders resp, NC.responseBody resp)
where where
reqInfo = requestInfo HT.methodGet (Just bucket) (Just object) reqInfo = requestInfo HT.methodGet (Just bucket) (Just object)
queryParams headers Nothing queryParams headers EPayload
putBucket :: Bucket -> Location -> Minio () putBucket :: Bucket -> Location -> Minio ()
putBucket bucket location = do putBucket bucket location = do
void $ executeRequest $ void $ executeRequest $
requestInfo HT.methodPut (Just bucket) Nothing [] [] $ requestInfo HT.methodPut (Just bucket) Nothing [] [] $
Just $ mkCreateBucketConfig location PayloadBS $ mkCreateBucketConfig location
maxSinglePutObjectSizeBytes :: Int64
maxSinglePutObjectSizeBytes = 5 * 1024 * 1024 * 1024
putObject :: Bucket -> Object -> [HT.Header] -> Int64
-> Int64 -> Handle -> Minio ()
putObject bucket object headers offset size h = do
-- check length is within single PUT object size.
when (size > maxSinglePutObjectSizeBytes) $
throwError $ MErrValidation $ MErrVSinglePUTSizeExceeded size
-- content-length header is automatically set by library.
void $ executeRequest $
requestInfo HT.methodPut (Just bucket) (Just object) [] headers $
PayloadH h offset size
deleteBucket :: Bucket -> Minio () deleteBucket :: Bucket -> Minio ()
deleteBucket bucket = do deleteBucket bucket = do
void $ executeRequest $ void $ executeRequest $
requestInfo HT.methodDelete (Just bucket) Nothing [] [] Nothing requestInfo HT.methodDelete (Just bucket) Nothing [] [] EPayload
deleteObject :: Bucket -> Object -> Minio () deleteObject :: Bucket -> Object -> Minio ()
deleteObject bucket object = do deleteObject bucket object = do
void $ executeRequest $ void $ executeRequest $
requestInfo HT.methodDelete (Just bucket) (Just object) [] [] Nothing requestInfo HT.methodDelete (Just bucket) (Just object) [] [] EPayload

View File

@ -3,9 +3,15 @@ import Protolude
import Test.Tasty import Test.Tasty
import Test.Tasty.HUnit import Test.Tasty.HUnit
-- import qualified System.IO as SIO
import Control.Monad.Trans.Resource (runResourceT) import Control.Monad.Trans.Resource (runResourceT)
-- import qualified Conduit as C
-- import Data.Conduit.Binary
import Network.Minio import Network.Minio
-- import Network.Minio.S3API
import XmlTests import XmlTests
main :: IO () main :: IO ()
@ -50,6 +56,18 @@ unitTests = testGroup "Unit tests"
step "Running test.." step "Running test.."
ret <- runResourceT $ runMinio mc $ getService ret <- runResourceT $ runMinio mc $ getService
isRight ret @? ("getService failure => " ++ show ret) isRight ret @? ("getService failure => " ++ show ret)
, testCaseSteps "Simple putObject works" $ \step -> do
step "Preparing..."
mc <- connect defaultConnectInfo
step "Running test.."
ret <- runResourceT $ runMinio mc $
fPutObject "testbucket" "lsb-release" "/etc/lsb-release"
-- h <- SIO.openBinaryFile "/etc/lsb-release" SIO.ReadMode
-- ret <- runResourceT $ runMinio mc $
-- putObject "testbucket" "lsb-release" [] 0 105 h
isRight ret @? ("putObject failure => " ++ show ret)
, testCase "Test mkCreateBucketConfig." testMkCreateBucketConfig , testCase "Test mkCreateBucketConfig." testMkCreateBucketConfig
] ]