{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeFamilies #-}
module Network.Minio.Data where
import Control.Concurrent.MVar (MVar)
import qualified Control.Concurrent.MVar as M
import qualified Control.Monad.Catch as MC
import Control.Monad.IO.Unlift (MonadUnliftIO, UnliftIO (..),
askUnliftIO, withUnliftIO)
import Control.Monad.Trans.Resource
import qualified Data.ByteString as B
import Data.CaseInsensitive (mk)
import Data.Default (Default (..))
import qualified Data.Map as Map
import qualified Data.Text as T
import Data.Time (defaultTimeLocale, formatTime)
import Network.HTTP.Client (defaultManagerSettings)
import qualified Network.HTTP.Conduit as NC
import Network.HTTP.Types (ByteRange, Header, Method, Query,
hRange)
import qualified Network.HTTP.Types as HT
import Network.Minio.Errors
import Text.XML
import GHC.Show (Show (..))
import Lib.Prelude
maxObjectSize :: Int64
maxObjectSize = 5 * 1024 * 1024 * oneMiB
minPartSize :: Int64
minPartSize = 64 * oneMiB
oneMiB :: Int64
oneMiB = 1024 * 1024
maxMultipartParts :: Int64
maxMultipartParts = 10000
awsRegionMap :: Map.Map Text Text
awsRegionMap = Map.fromList [
("us-east-1", "s3.amazonaws.com")
, ("us-east-2", "s3-us-east-2.amazonaws.com")
, ("us-west-1", "s3-us-west-1.amazonaws.com")
, ("us-east-2", "s3-us-west-2.amazonaws.com")
, ("ca-central-1", "s3-ca-central-1.amazonaws.com")
, ("ap-south-1", "s3-ap-south-1.amazonaws.com")
, ("ap-northeast-1", "s3-ap-northeast-1.amazonaws.com")
, ("ap-northeast-2", "s3-ap-northeast-2.amazonaws.com")
, ("ap-southeast-1", "s3-ap-southeast-1.amazonaws.com")
, ("ap-southeast-2", "s3-ap-southeast-2.amazonaws.com")
, ("eu-west-1", "s3-eu-west-1.amazonaws.com")
, ("eu-west-2", "s3-eu-west-2.amazonaws.com")
, ("eu-central-1", "s3-eu-central-1.amazonaws.com")
, ("sa-east-1", "s3-sa-east-1.amazonaws.com")
]
data ConnectInfo = ConnectInfo {
connectHost :: Text
, connectPort :: Int
, connectAccessKey :: Text
, connectSecretKey :: Text
, connectIsSecure :: Bool
, connectRegion :: Region
, connectAutoDiscoverRegion :: Bool
} deriving (Eq, Show)
instance Default ConnectInfo where
def = ConnectInfo "localhost" 9000 "minio" "minio123" False "us-east-1" True
awsCI :: ConnectInfo
awsCI = def {
connectHost = "s3.amazonaws.com"
, connectPort = 443
, connectAccessKey = ""
, connectSecretKey = ""
, connectIsSecure = True
}
awsWithRegionCI :: Region -> Bool -> ConnectInfo
awsWithRegionCI region autoDiscoverRegion =
let host = maybe "s3.amazonaws.com" identity $
Map.lookup region awsRegionMap
in awsCI {
connectHost = host
, connectRegion = region
, connectAutoDiscoverRegion = autoDiscoverRegion
}
minioPlayCI :: ConnectInfo
minioPlayCI = def {
connectHost = "play.minio.io"
, connectPort = 9000
, connectAccessKey = "Q3AM3UQ867SPQQA43P2F"
, connectSecretKey = "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
, connectIsSecure = True
, connectAutoDiscoverRegion = False
}
minioCI :: Text -> Int -> Bool -> ConnectInfo
minioCI host port isSecure = def {
connectHost = host
, connectPort = port
, connectRegion = "us-east-1"
, connectIsSecure = isSecure
, connectAutoDiscoverRegion = False
}
type Bucket = Text
type Object = Text
type Region = Text
type ETag = Text
data PutObjectOptions = PutObjectOptions {
pooContentType :: Maybe Text
, pooContentEncoding :: Maybe Text
, pooContentDisposition :: Maybe Text
, pooCacheControl :: Maybe Text
, pooContentLanguage :: Maybe Text
, pooStorageClass :: Maybe Text
, pooUserMetadata :: [(Text, Text)]
, pooNumThreads :: Maybe Word
} deriving (Show, Eq)
instance Default PutObjectOptions where
def = PutObjectOptions def def def def def def [] def
addXAmzMetaPrefix :: Text -> Text
addXAmzMetaPrefix s = do
if (T.isPrefixOf "x-amz-meta-" s)
then s
else T.concat ["x-amz-meta-", s]
mkHeaderFromMetadata :: [(Text, Text)] -> [HT.Header]
mkHeaderFromMetadata = map (\(x, y) -> (mk $ encodeUtf8 $ addXAmzMetaPrefix $ T.toLower x, encodeUtf8 y))
pooToHeaders :: PutObjectOptions -> [HT.Header]
pooToHeaders poo = userMetadata
++ (catMaybes $ map tupToMaybe (zipWith (,) names values))
where
tupToMaybe (k, Just v) = Just (k, v)
tupToMaybe (_, Nothing) = Nothing
userMetadata = mkHeaderFromMetadata $ pooUserMetadata poo
names = ["content-type",
"content-encoding",
"content-disposition",
"content-language",
"cache-control",
"x-amz-storage-class"]
values = map (fmap encodeUtf8 . (poo &))
[pooContentType, pooContentEncoding,
pooContentDisposition, pooContentLanguage,
pooCacheControl, pooStorageClass]
data BucketInfo = BucketInfo {
biName :: Bucket
, biCreationDate :: UTCTime
} deriving (Show, Eq)
type PartNumber = Int16
type UploadId = Text
type PartTuple = (PartNumber, ETag)
data ListPartsResult = ListPartsResult {
lprHasMore :: Bool
, lprNextPart :: Maybe Int
, lprParts :: [ObjectPartInfo]
} deriving (Show, Eq)
data ObjectPartInfo = ObjectPartInfo {
opiNumber :: PartNumber
, opiETag :: ETag
, opiSize :: Int64
, opiModTime :: UTCTime
} deriving (Show, Eq)
data ListUploadsResult = ListUploadsResult {
lurHasMore :: Bool
, lurNextKey :: Maybe Text
, lurNextUpload :: Maybe Text
, lurUploads :: [(Object, UploadId, UTCTime)]
, lurCPrefixes :: [Text]
} deriving (Show, Eq)
data UploadInfo = UploadInfo {
uiKey :: Object
, uiUploadId :: UploadId
, uiInitTime :: UTCTime
, uiSize :: Int64
} deriving (Show, Eq)
data ListObjectsResult = ListObjectsResult {
lorHasMore :: Bool
, lorNextToken :: Maybe Text
, lorObjects :: [ObjectInfo]
, lorCPrefixes :: [Text]
} deriving (Show, Eq)
data ListObjectsV1Result = ListObjectsV1Result {
lorHasMore' :: Bool
, lorNextMarker :: Maybe Text
, lorObjects' :: [ObjectInfo]
, lorCPrefixes' :: [Text]
} deriving (Show, Eq)
data ObjectInfo = ObjectInfo {
oiObject :: Object
, oiModTime :: UTCTime
, oiETag :: ETag
, oiSize :: Int64
, oiMetadata :: Map.Map Text Text
} deriving (Show, Eq)
data SourceInfo = SourceInfo {
srcBucket :: Text
, srcObject :: Text
, srcRange :: Maybe (Int64, Int64)
, srcIfMatch :: Maybe Text
, srcIfNoneMatch :: Maybe Text
, srcIfModifiedSince :: Maybe UTCTime
, srcIfUnmodifiedSince :: Maybe UTCTime
} deriving (Show, Eq)
instance Default SourceInfo where
def = SourceInfo "" "" def def def def def
data DestinationInfo = DestinationInfo {
dstBucket :: Text
, dstObject :: Text
} deriving (Show, Eq)
instance Default DestinationInfo where
def = DestinationInfo "" ""
data GetObjectOptions = GetObjectOptions {
gooRange :: Maybe ByteRange
, gooIfMatch :: Maybe ETag
, gooIfNoneMatch :: Maybe ETag
, gooIfUnmodifiedSince :: Maybe UTCTime
, gooIfModifiedSince :: Maybe UTCTime
} deriving (Show, Eq)
instance Default GetObjectOptions where
def = GetObjectOptions def def def def def
gooToHeaders :: GetObjectOptions -> [HT.Header]
gooToHeaders goo = rangeHdr ++ zip names values
where
names = ["If-Match",
"If-None-Match",
"If-Unmodified-Since",
"If-Modified-Since"]
values = mapMaybe (fmap encodeUtf8 . (goo &))
[gooIfMatch, gooIfNoneMatch,
fmap formatRFC1123 . gooIfUnmodifiedSince,
fmap formatRFC1123 . gooIfModifiedSince]
rangeHdr = maybe [] (\a -> [(hRange, HT.renderByteRanges [a])])
$ gooRange goo
data Event = ObjectCreated
| ObjectCreatedPut
| ObjectCreatedPost
| ObjectCreatedCopy
| ObjectCreatedMultipartUpload
| ObjectRemoved
| ObjectRemovedDelete
| ObjectRemovedDeleteMarkerCreated
| ReducedRedundancyLostObject
deriving (Eq)
instance Show Event where
show ObjectCreated = "s3:ObjectCreated:*"
show ObjectCreatedPut = "s3:ObjectCreated:Put"
show ObjectCreatedPost = "s3:ObjectCreated:Post"
show ObjectCreatedCopy = "s3:ObjectCreated:Copy"
show ObjectCreatedMultipartUpload = "s3:ObjectCreated:MultipartUpload"
show ObjectRemoved = "s3:ObjectRemoved:*"
show ObjectRemovedDelete = "s3:ObjectRemoved:Delete"
show ObjectRemovedDeleteMarkerCreated = "s3:ObjectRemoved:DeleteMarkerCreated"
show ReducedRedundancyLostObject = "s3:ReducedRedundancyLostObject"
textToEvent :: Text -> Maybe Event
textToEvent t = case t of
"s3:ObjectCreated:*" -> Just ObjectCreated
"s3:ObjectCreated:Put" -> Just ObjectCreatedPut
"s3:ObjectCreated:Post" -> Just ObjectCreatedPost
"s3:ObjectCreated:Copy" -> Just ObjectCreatedCopy
"s3:ObjectCreated:MultipartUpload" -> Just ObjectCreatedMultipartUpload
"s3:ObjectRemoved:*" -> Just ObjectRemoved
"s3:ObjectRemoved:Delete" -> Just ObjectRemovedDelete
"s3:ObjectRemoved:DeleteMarkerCreated" -> Just ObjectRemovedDeleteMarkerCreated
"s3:ReducedRedundancyLostObject" -> Just ReducedRedundancyLostObject
_ -> Nothing
data Filter = Filter
{ fFilter :: FilterKey
} deriving (Show, Eq)
instance Default Filter where
def = Filter def
data FilterKey = FilterKey
{ fkKey :: FilterRules
} deriving (Show, Eq)
instance Default FilterKey where
def = FilterKey def
data FilterRules = FilterRules
{ frFilterRules :: [FilterRule]
} deriving (Show, Eq)
instance Default FilterRules where
def = FilterRules []
data FilterRule = FilterRule
{ frName :: Text
, frValue :: Text
} deriving (Show, Eq)
type Arn = Text
data NotificationConfig = NotificationConfig
{ ncId :: Text
, ncArn :: Arn
, ncEvents :: [Event]
, ncFilter :: Filter
} deriving (Show, Eq)
data Notification = Notification
{ nQueueConfigurations :: [NotificationConfig]
, nTopicConfigurations :: [NotificationConfig]
, nCloudFunctionConfigurations :: [NotificationConfig]
} deriving (Eq, Show)
instance Default Notification where
def = Notification [] [] []
data Payload = PayloadBS ByteString
| PayloadH Handle
Int64
Int64
instance Default Payload where
def = PayloadBS ""
data RequestInfo = RequestInfo {
riMethod :: Method
, riBucket :: Maybe Bucket
, riObject :: Maybe Object
, riQueryParams :: Query
, riHeaders :: [Header]
, riPayload :: Payload
, riPayloadHash :: Maybe ByteString
, riRegion :: Maybe Region
, riNeedsLocation :: Bool
}
instance Default RequestInfo where
def = RequestInfo HT.methodGet def def def def def Nothing def True
getPathFromRI :: RequestInfo -> ByteString
getPathFromRI ri =
let
b = riBucket ri
o = riObject ri
segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
in
B.concat ["/", B.intercalate "/" segments]
type UrlExpiry = Int
type RegionMap = Map.Map Bucket Region
newtype Minio a = Minio {
unMinio :: ReaderT MinioConn (ResourceT IO) a
}
deriving (
Functor
, Applicative
, Monad
, MonadIO
, MonadReader MinioConn
, MonadThrow
, MonadCatch
, MonadResource
)
instance MonadUnliftIO Minio where
askUnliftIO = Minio $ ReaderT $ \r ->
withUnliftIO $ \u ->
return (UnliftIO (unliftIO u . flip runReaderT r . unMinio))
data MinioConn = MinioConn
{ mcConnInfo :: ConnectInfo
, mcConnManager :: NC.Manager
, mcRegionMap :: MVar RegionMap
}
connect :: ConnectInfo -> IO MinioConn
connect ci = do
let settings | connectIsSecure ci = NC.tlsManagerSettings
| otherwise = defaultManagerSettings
mgr <- NC.newManager settings
rMapMVar <- M.newMVar Map.empty
return $ MinioConn ci mgr rMapMVar
runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
runMinio ci m = do
conn <- liftIO $ connect ci
runResourceT . flip runReaderT conn . unMinio $
fmap Right m `MC.catches`
[ MC.Handler handlerServiceErr
, MC.Handler handlerHE
, MC.Handler handlerFE
, MC.Handler handlerValidation
]
where
handlerServiceErr = return . Left . MErrService
handlerHE = return . Left . MErrHTTP
handlerFE = return . Left . MErrIO
handlerValidation = return . Left . MErrValidation
s3Name :: Text -> Name
s3Name s = Name s (Just "http://s3.amazonaws.com/doc/2006-03-01/") Nothing
formatRFC1123 :: UTCTime -> T.Text
formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"