{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {- Copyright (c) 2016-2019 Herbert Valerio Riedel This file is free software: you may copy, redistribute and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This file is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program (see `LICENSE`). If not, see . -} -- | -- Copyright: © Herbert Valerio Riedel 2016-2019 -- SPDX-License-Identifier: GPL-3.0-or-later module Network.S3.Types ( module Network.S3.Types ) where import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Short as BSS import Data.Char import qualified Data.Text.Encoding as T import qualified Data.Text.Short as TS import Internal import qualified Network.Http.Client as HC class XsdString a where fromXsdString :: Text -> a instance XsdString Text where fromXsdString = id instance XsdString ShortText where fromXsdString = TS.fromText instance XsdString ShortByteString where fromXsdString = TS.toShortByteString . TS.fromText instance XsdString ByteString where fromXsdString = T.encodeUtf8 type UrlPath = ByteString -- | Content-type newtype CType = CType ShortText deriving (Eq,Show,Typeable,Generic,NFData,Hashable) -- | Unspecified 'CType' noCType :: CType noCType = CType mempty -- | Configure S3 endpoint data S3Cfg = S3Cfg { s3cfgBaseUrl :: !HC.URL -- ^ Service endpoint (i.e without 'BucketId'); Only scheme, host and port are used currently -- , s3cfgPathStyle :: !Bool -- ^ use path-style access mode (i.e. instead of virtual-hosted style ) , s3cfgRegion :: !ByteString -- ^ E.g. @"us-east-1"@ this is currently only used for computing the signature when 's3cfgSigVersion' is set to 'SignatureV4' , s3cfgSigVersion :: !SignatureVersion -- ^ Which signature algorithm to use for authentication; 'SignatureV4' is recommended unless there's reason to use the legacy 'SignatureV2' variant. , s3cfgEncodingUrl :: !Bool -- ^ Enable use of @encoding=url@ feature for some operations -- -- This is only needed when object keys contain Unicode code-points not representable in XML 1.0; the XML 1.0 representable code-points are -- -- > Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] -- -- Note that some S3 server implementations exhibit bugs when using LF or CR characters in object keys. -- -- Note also that some S3 implementation have been observed to incorrectly implement @encoding=url@ so it's generally advisable to disable this feature unless there's actual need and it's been confirmed that the S3 server implementatio implements it correctly. , s3cfgDebug :: !Bool -- ^ Enable protocol debugging output to stdout } deriving (Show,Typeable,Generic) instance NFData S3Cfg -- | Default 'S3Cfg' value with recommended/default settings, i.e. -- -- >>> defaultS3Cfg -- S3Cfg {s3cfgBaseUrl = "", s3cfgRegion = "us-east-1", s3cfgSigVersion = SignatureV4, s3cfgEncodingUrl = False, s3cfgDebug = False} -- -- __NOTE__: At the very least you have to override the 's3cfgBaseUrl' field. defaultS3Cfg :: S3Cfg defaultS3Cfg = S3Cfg { s3cfgBaseUrl = "" -- , s3cfgPathStyle = True , s3cfgRegion = "us-east-1" , s3cfgSigVersion = SignatureV4 , s3cfgEncodingUrl = False , s3cfgDebug = False } -- | Denotes version of the S3 request signing algorithm data SignatureVersion = SignatureV2 -- ^ Legacy HMAC-SHA1/MD5 based signing algorithm | SignatureV4 -- ^ Current HMAC-SHA256 based signing algorithm (recommended) deriving (Eq,Show,Typeable,Generic) instance NFData SignatureVersion -- | S3 Credentials -- -- We use memory pinned 'ByteString's because we don't want to have the credential data copied around more than necessary. data Credentials = Credentials { s3AccessKey :: !ByteString -- ^ 'mempty' denotes anonymous access (see also 'noCredentials') , s3SecretKey :: !ByteString } deriving (Eq,Show,Typeable,Generic) instance NFData Credentials -- | Anonymous access noCredentials :: Credentials noCredentials = Credentials "" "" isAnonCredentials :: Credentials -> Bool isAnonCredentials (Credentials akey _) = BS.null akey -- | S3 Bucket identifier -- -- newtype BucketId = BucketId ShortByteString -- ^ Must be valid as DNS name component; S3 server implementations may have additional restrictions (see e.g. AWS S3's ) deriving (Eq,Ord,Show,NFData,Generic,Typeable,XsdString,Hashable) -- | The name for a key is a non-empty sequence of Unicode characters whose UTF-8 encoding is at most 1024 bytes long. -- -- See also remarks in 's3cfgEncodingUrl' about permissible code-points. -- -- See also AWS S3's documentation on newtype ObjKey = ObjKey ShortText deriving (Show,Eq,Ord,Typeable,Generic,NFData,Hashable) unObjKey :: ObjKey -> ShortText unObjKey (ObjKey k) = k -- | Represents the /null/ (or empty) 'ObjKey' nullObjKey :: ObjKey nullObjKey = ObjKey mempty -- | Test whether 'ObjKey' is the 'nullObjKey' isNullObjKey :: ObjKey -> Bool isNullObjKey = TS.null . unObjKey ---------------------------------------------------------------------------- -- | Denotes an data ETag = ETag !ShortByteString | ETagMD5 !MD5Val -- ^ This constructor will be used if the ETag looks like a proper MD5 based ETag deriving (Show,Eq,Ord,Typeable,Generic) instance NFData ETag where rnf !_ = () instance Hashable ETag {- entity-tag = [ weak ] opaque-tag weak = %x57.2F ; "W/", case-sensitive opaque-tag = DQUOTE *etagc DQUOTE etagc = %x21 / %x23-7E / obs-text ; VCHAR except double quotes, plus obs-text obs-text = %x80-FF -} etagToBS :: ETag -> ByteString etagToBS (ETag etag) = BSS.fromShort etag etagToBS (ETagMD5 md5) = mconcat [ "\"", md5hex md5, "\"" ] mkETag :: ByteString -> ETag mkETag bs | BS.length bs == 34, BC8.head bs == '"', BC8.last bs == '"' , BC8.all (\c -> isHexDigit c && not (isUpper c)) (BS.init (BS.tail bs)) , Just md5 <- md5unhex (BS.init (BS.tail bs)) = ETagMD5 md5 | otherwise = ETag (BSS.toShort bs) ---------------------------------------------------------------------------- -- | Object Metadata data ObjMetaInfo = OMI { omiKey :: !ObjKey , omiEtag :: !ETag , omiSize :: !Int64 , omiOwnerId :: !(Maybe ShortText) , omiLastModified :: !UTCTime } deriving (Eq,Ord,Show,Typeable,Generic) instance NFData ObjMetaInfo ---------------------------------------------------------------------------- -- | Conditional Request -- -- Note that S3 server implementations vary in their support for -- conditional requests -- data Condition = IfExists -- ^ @If-Match: *@ | IfNotExists -- ^ @If-None-Match: *@ | IfMatch !ETag -- ^ @If-Match: ...@ | IfNotMatch !ETag -- ^ @If-None-Match: ...@ deriving (Eq,Show,Typeable,Generic) instance NFData Condition setConditionHeader :: Condition -> HC.RequestBuilder () setConditionHeader = \case IfExists -> HC.setHeader "If-Match" "*" IfNotExists -> HC.setHeader "If-None-Match" "*" IfMatch etag -> HC.setHeader "If-Match" (etagToBS etag) IfNotMatch etag -> HC.setHeader "If-None-Match" (etagToBS etag)