module Network.Minio.Data where
import Control.Monad.Base
import qualified Control.Monad.Catch as MC
import Control.Monad.Trans.Control
import Control.Monad.Trans.Resource
import qualified Data.ByteString as B
import Data.Default (Default(..))
import qualified Data.Map as Map
import qualified Data.Text as T
import Data.Time (formatTime, defaultTimeLocale)
import Network.HTTP.Client (defaultManagerSettings)
import qualified Network.HTTP.Conduit as NC
import Network.HTTP.Types (Method, Header, Query)
import qualified Network.HTTP.Types as HT
import Network.Minio.Errors
import Text.XML
import GHC.Show (Show(..))
import Lib.Prelude
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 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
} deriving (Show, Eq)
data CopyPartSource = CopyPartSource {
cpSource :: Text
, cpSourceRange :: Maybe (Int64, Int64)
, cpSourceIfMatch :: Maybe Text
, cpSourceIfNoneMatch :: Maybe Text
, cpSourceIfUnmodifiedSince :: Maybe UTCTime
, cpSourceIfModifiedSince :: Maybe UTCTime
} deriving (Show, Eq)
instance Default CopyPartSource where
def = CopyPartSource "" def def def def def
cpsToHeaders :: CopyPartSource -> [HT.Header]
cpsToHeaders cps = ("x-amz-copy-source", encodeUtf8 $ cpSource cps) :
rangeHdr ++ zip names values
where
names = ["x-amz-copy-source-if-match", "x-amz-copy-source-if-none-match",
"x-amz-copy-source-if-unmodified-since",
"x-amz-copy-source-if-modified-since"]
values = mapMaybe (fmap encodeUtf8 . (cps &))
[cpSourceIfMatch, cpSourceIfNoneMatch,
fmap formatRFC1123 . cpSourceIfUnmodifiedSince,
fmap formatRFC1123 . cpSourceIfModifiedSince]
rangeHdr = ("x-amz-copy-source-range",)
. HT.renderByteRanges
. (:[])
. uncurry HT.ByteRangeFromTo
<$> map (both fromIntegral) (maybeToList $ cpSourceRange cps)
cpsToObject :: CopyPartSource -> Maybe (Bucket, Object)
cpsToObject cps = do
[_, bucket, object] <- Just splits
return (bucket, object)
where
splits = T.splitOn "/" $ cpSource cps
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 (StateT RegionMap (ResourceT IO)) a
}
deriving (
Functor
, Applicative
, Monad
, MonadIO
, MonadReader MinioConn
, MonadState RegionMap
, MonadThrow
, MonadCatch
, MonadBase IO
, MonadResource
)
instance MonadBaseControl IO Minio where
type StM Minio a = (a, RegionMap)
liftBaseWith f = Minio $ liftBaseWith $ \q -> f (q . unMinio)
restoreM = Minio . restoreM
data MinioConn = MinioConn {
mcConnInfo :: ConnectInfo
, mcConnManager :: NC.Manager
}
connect :: ConnectInfo -> IO MinioConn
connect ci = do
let settings = bool defaultManagerSettings NC.tlsManagerSettings $
connectIsSecure ci
mgr <- NC.newManager settings
return $ MinioConn ci mgr
runMinio :: ConnectInfo -> Minio a -> IO (Either MinioErr a)
runMinio ci m = do
conn <- liftIO $ connect ci
runResourceT . flip evalStateT Map.empty . 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"