bindings/haskell/haskell-src/OpenDAL.hs (511 lines of code) (raw):
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing,
-- software distributed under the License is distributed on an
-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-- KIND, either express or implied. See the License for the
-- specific language governing permissions and limitations
-- under the License.
-- |
-- Module : OpenDAL
-- Description : Haskell bindings for OpenDAL
-- Copyright : (c) 2023 OpenDAL
-- License : Apache-2.0
-- Maintainer : OpenDAL Contributors <dev@opendal.apache.org>"
-- Stability : experimental
-- Portability : non - portable (GHC extensions)
--
-- This module provides Haskell bindings for OpenDAL.
module OpenDAL
( -- * Operator APIs
-- ** Types
OperatorConfig (..),
Operator,
Lister,
OpenDALError (..),
ErrorCode (..),
EntryMode (..),
Metadata (..),
OperatorT (..),
MonadOperation (..),
-- ** Functions
runOp,
newOperator,
-- * Lister APIs
nextLister,
-- * Operator Raw APIs
-- $raw-operations
readOpRaw,
writeOpRaw,
isExistOpRaw,
createDirOpRaw,
copyOpRaw,
renameOpRaw,
deleteOpRaw,
statOpRaw,
listOpRaw,
scanOpRaw,
)
where
import Colog (LogAction, Message, Msg (Msg), (<&))
import Control.Monad.Except (ExceptT, MonadError, MonadTrans, runExceptT, throwError)
import Control.Monad.Reader (MonadIO, MonadReader, ReaderT, ask, liftIO, runReaderT)
import Control.Monad.Trans (MonadTrans (lift))
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.String (IsString (fromString))
import Data.Text (pack)
import Data.Time (UTCTime, parseTimeM, zonedTimeToUTC)
import Data.Time.Format (defaultTimeLocale)
import Foreign
import Foreign.C.String
import GHC.Stack (emptyCallStack)
import OpenDAL.FFI
-- | `OperatorConfig` is the configuration for an `Operator`.
-- We recommend using `OverloadedStrings` to construct a default config.
--
-- For example:
--
-- default config
--
-- @
-- newOperator "memory"
-- @
--
-- custom services config
--
-- @
-- newOperator "memory" {ocConfig = HashMap.fromList [("root", "/tmp")]}
-- @
--
-- enable logging
--
-- @
-- newOperator "memory" {ocLogAction = Just simpleMessageAction}
-- @
data OperatorConfig = OperatorConfig
{ -- | The scheme of the operator. For example, "s3" or "gcs".
ocScheme :: String,
-- | The config for the operator services. For example, "endpoint" and "access_key_id" for s3. The config is same as rust core.
ocConfig :: HashMap String String,
-- | The log action for the operator. If it's `Nothing`, the operator won't enable logging.
ocLogAction :: Maybe (LogAction IO Message)
}
instance IsString OperatorConfig where
fromString s = OperatorConfig s HashMap.empty Nothing
-- | `Operator` is the entry for all public blocking APIs.
-- Create an `Operator` with `newOperator`.
newtype Operator = Operator (ForeignPtr RawOperator)
-- | `Lister` is designed to list entries at given path in a blocking manner.
-- Users can construct Lister by `listOp` or `scanOp`.
newtype Lister = Lister (ForeignPtr RawLister)
-- | Represents the possible error codes that can be returned by OpenDAL.
data ErrorCode
= -- | An error occurred in the FFI layer.
FFIError
| -- | OpenDAL don't know what happened here, and no actions other than just returning it back. For example, s3 returns an internal service error.
Unexpected
| -- | Underlying service doesn't support this operation.
Unsupported
| -- | The config for backend is invalid.
ConfigInvalid
| -- | The given path is not found.
NotFound
| -- | The given path doesn't have enough permission for this operation.
PermissionDenied
| -- | The given path is a directory.
IsADirectory
| -- | The given path is not a directory.
NotADirectory
| -- | The given path already exists thus we failed to the specified operation on it.
AlreadyExists
| -- | Requests that sent to this path is over the limit, please slow down.
RateLimited
| -- | The given file paths are same.
IsSameFile
deriving (Eq, Show)
-- | Represents an error that can occur when using OpenDAL.
data OpenDALError = OpenDALError
{ -- | The error code.
errorCode :: ErrorCode,
-- | The error message.
message :: String
}
deriving (Eq, Show)
-- | Represents the mode of an entry in a storage system (e.g., file or directory).
data EntryMode = File | Dir | Unknown deriving (Eq, Show)
-- | Represents metadata for an entry in a storage system.
data Metadata = Metadata
{ -- | The mode of the entry.
mMode :: EntryMode,
-- | The cache control of the entry.
mCacheControl :: Maybe String,
-- | The content disposition of the entry.
mContentDisposition :: Maybe String,
-- | The content length of the entry.
mContentLength :: Integer,
-- | The content MD5 of the entry.
mContentMD5 :: Maybe String,
-- | The content type of the entry.
mContentType :: Maybe String,
-- | The ETag of the entry.
mETag :: Maybe String,
-- | The last modified time of the entry.
mLastModified :: Maybe UTCTime
}
deriving (Eq, Show)
-- | @newtype@ wrapper 'ReaderT' that keeps 'Operator' in its context.
newtype OperatorT m a = OperatorT
{runOperatorT :: ReaderT Operator (ExceptT OpenDALError m) a}
deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader Operator, MonadError OpenDALError)
instance MonadTrans OperatorT where
lift = OperatorT . lift . lift
-- | A type class for monads that can perform OpenDAL operations.
class (Monad m) => MonadOperation m where
-- | Read the whole path into a bytes.
readOp :: String -> m ByteString
-- | Write bytes into given path.
writeOp :: String -> ByteString -> m ()
-- | Check if this path exists or not.
isExistOp :: String -> m Bool
-- | Create a dir at given path.
createDirOp :: String -> m ()
-- | Copy a file from srcPath to dstPath.
copyOp :: String -> String -> m ()
-- | Rename a file from srcPath to dstPath.
renameOp :: String -> String -> m ()
-- | Delete given path.
deleteOp :: String -> m ()
-- | Get given path’s metadata without cache directly.
statOp :: String -> m Metadata
-- | List current dir path.
-- This function will create a new handle to list entries.
-- An error will be returned if path doesn’t end with /.
listOp :: String -> m Lister
-- | List dir in flat way.
-- Also, this function can be used to list a prefix.
-- An error will be returned if given path doesn’t end with /.
scanOp :: String -> m Lister
instance (MonadIO m) => MonadOperation (OperatorT m) where
readOp path = do
op <- ask
result <- liftIO $ readOpRaw op path
either throwError return result
writeOp path byte = do
op <- ask
result <- liftIO $ writeOpRaw op path byte
either throwError return result
isExistOp path = do
op <- ask
result <- liftIO $ isExistOpRaw op path
either throwError return result
createDirOp path = do
op <- ask
result <- liftIO $ createDirOpRaw op path
either throwError return result
copyOp src dst = do
op <- ask
result <- liftIO $ copyOpRaw op src dst
either throwError return result
renameOp src dst = do
op <- ask
result <- liftIO $ renameOpRaw op src dst
either throwError return result
deleteOp path = do
op <- ask
result <- liftIO $ deleteOpRaw op path
either throwError return result
statOp path = do
op <- ask
result <- liftIO $ statOpRaw op path
either throwError return result
listOp path = do
op <- ask
result <- liftIO $ listOpRaw op path
either throwError return result
scanOp path = do
op <- ask
result <- liftIO $ scanOpRaw op path
either throwError return result
-- helper functions
byteSliceToByteString :: ByteSlice -> IO ByteString
byteSliceToByteString (ByteSlice bsDataPtr len) = BS.packCStringLen (bsDataPtr, fromIntegral len)
parseErrorCode :: Int -> ErrorCode
parseErrorCode 1 = FFIError
parseErrorCode 2 = Unexpected
parseErrorCode 3 = Unsupported
parseErrorCode 4 = ConfigInvalid
parseErrorCode 5 = NotFound
parseErrorCode 6 = PermissionDenied
parseErrorCode 7 = IsADirectory
parseErrorCode 8 = NotADirectory
parseErrorCode 9 = AlreadyExists
parseErrorCode 10 = RateLimited
parseErrorCode 11 = IsSameFile
parseErrorCode _ = FFIError
parseEntryMode :: Int -> EntryMode
parseEntryMode 0 = File
parseEntryMode 1 = Dir
parseEntryMode _ = Unknown
parseCString :: CString -> IO (Maybe String)
parseCString value | value == nullPtr = return Nothing
parseCString value = do
value' <- peekCString value
free value
return $ Just value'
parseTime :: String -> Maybe UTCTime
parseTime time = zonedTimeToUTC <$> parseTimeM True defaultTimeLocale "%Y-%m-%dT%H:%M:%S%Q%z" time
parseFFIMetadata :: FFIMetadata -> IO Metadata
parseFFIMetadata (FFIMetadata mode cacheControl contentDisposition contentLength contentMD5 contentType eTag lastModified) = do
let mode' = parseEntryMode $ fromIntegral mode
cacheControl' <- parseCString cacheControl
contentDisposition' <- parseCString contentDisposition
let contentLength' = toInteger contentLength
contentMD5' <- parseCString contentMD5
contentType' <- parseCString contentType
eTag' <- parseCString eTag
lastModified' <- (>>= parseTime) <$> parseCString lastModified
return $
Metadata
{ mMode = mode',
mCacheControl = cacheControl',
mContentDisposition = contentDisposition',
mContentLength = contentLength',
mContentMD5 = contentMD5',
mContentType = contentType',
mETag = eTag',
mLastModified = lastModified'
}
-- Exported functions
-- | Runner for 'OperatorT' monad.
-- This function will run given 'OperatorT' monad with given 'Operator'.
--
-- Let's see an example:
--
-- @
-- operation :: MonadOperation m => m ()
-- operation = __do__
-- writeOp op "key1" "value1"
-- writeOp op "key2" "value2"
-- value1 <- readOp op "key1"
-- value2 <- readOp op "key2"
-- @
--
-- You can run this operation with 'runOp' function:
--
-- @
-- runOp operator operation
-- @
runOp :: Operator -> OperatorT m a -> m (Either OpenDALError a)
runOp op = runExceptT . flip runReaderT op . runOperatorT
{-# INLINE runOp #-}
-- | Creates a new OpenDAL operator via `OperatorConfig`.
newOperator :: OperatorConfig -> IO (Either OpenDALError Operator)
newOperator (OperatorConfig scheme hashMap maybeLogger) = do
let keysAndValues = HashMap.toList hashMap
withCString scheme $ \cScheme ->
withMany withCString (map fst keysAndValues) $ \cKeys ->
withMany withCString (map snd keysAndValues) $ \cValues ->
allocaArray (length keysAndValues) $ \cKeysPtr ->
allocaArray (length keysAndValues) $ \cValuesPtr ->
alloca $ \ffiResultPtr -> do
logFnPtr <- case maybeLogger of
Just logger -> wrapLogFn (logFn logger)
Nothing -> return nullFunPtr
pokeArray cKeysPtr cKeys
pokeArray cValuesPtr cValues
c_via_map_ffi cScheme cKeysPtr cValuesPtr (fromIntegral $ length keysAndValues) logFnPtr ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then do
op <- Operator <$> newForeignPtr c_free_operator (dataPtr ffiResult)
return $ Right op
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
where
logFn logger enumSeverity cStr = do
str <- peekCString cStr
logger <& Msg (toEnum (fromIntegral enumSeverity)) emptyCallStack (pack str)
-- $raw-operations
-- Functions for performing raw OpenDAL operations are defined below.
-- These functions are not meant to be used directly in most cases.
-- Instead, use the high-level interface provided by the 'MonadOperation' type class.
-- | Read the whole path into a bytes.
readOpRaw :: Operator -> String -> IO (Either OpenDALError ByteString)
readOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
withCString path $ \cPath ->
alloca $ \ffiResultPtr -> do
c_blocking_read opptr cPath ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then do
byteslice <- peek $ dataPtr ffiResult
byte <- byteSliceToByteString byteslice
c_free_byteslice (bsData byteslice) (bsLen byteslice)
return $ Right byte
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
-- | Write bytes into given path.
writeOpRaw :: Operator -> String -> ByteString -> IO (Either OpenDALError ())
writeOpRaw (Operator op) path byte = withForeignPtr op $ \opptr ->
withCString path $ \cPath ->
BS.useAsCStringLen byte $ \(cByte, len) ->
alloca $ \ffiResultPtr -> do
c_blocking_write opptr cPath cByte (fromIntegral len) ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then return $ Right ()
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
-- | Check if this path exists or not.
isExistOpRaw :: Operator -> String -> IO (Either OpenDALError Bool)
isExistOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
withCString path $ \cPath ->
alloca $ \ffiResultPtr -> do
c_blocking_is_exist opptr cPath ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then do
val <- peek $ dataPtr ffiResult
let isExist = val /= 0
return $ Right isExist
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
-- | Create a dir at given path.
createDirOpRaw :: Operator -> String -> IO (Either OpenDALError ())
createDirOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
withCString path $ \cPath ->
alloca $ \ffiResultPtr -> do
c_blocking_create_dir opptr cPath ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then return $ Right ()
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
-- | Copy a file from srcPath to dstPath.
copyOpRaw :: Operator -> String -> String -> IO (Either OpenDALError ())
copyOpRaw (Operator op) srcPath dstPath = withForeignPtr op $ \opptr ->
withCString srcPath $ \cSrcPath ->
withCString dstPath $ \cDstPath ->
alloca $ \ffiResultPtr -> do
c_blocking_copy opptr cSrcPath cDstPath ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then return $ Right ()
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
-- | Rename a file from srcPath to dstPath.
renameOpRaw :: Operator -> String -> String -> IO (Either OpenDALError ())
renameOpRaw (Operator op) srcPath dstPath = withForeignPtr op $ \opptr ->
withCString srcPath $ \cSrcPath ->
withCString dstPath $ \cDstPath ->
alloca $ \ffiResultPtr -> do
c_blocking_rename opptr cSrcPath cDstPath ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then return $ Right ()
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
-- | Delete given path.
deleteOpRaw :: Operator -> String -> IO (Either OpenDALError ())
deleteOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
withCString path $ \cPath ->
alloca $ \ffiResultPtr -> do
c_blocking_delete opptr cPath ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then return $ Right ()
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
-- | Get given path’s metadata without cache directly.
statOpRaw :: Operator -> String -> IO (Either OpenDALError Metadata)
statOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
withCString path $ \cPath ->
alloca $ \ffiResultPtr -> do
c_blocking_stat opptr cPath ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then do
ffimatadata <- peek $ dataPtr ffiResult
metadata <- parseFFIMetadata ffimatadata
return $ Right metadata
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
-- | List current dir path.
-- This function will create a new handle to list entries.
-- An error will be returned if path doesn’t end with /.
listOpRaw :: Operator -> String -> IO (Either OpenDALError Lister)
listOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
withCString path $ \cPath ->
alloca $ \ffiResultPtr -> do
c_blocking_list opptr cPath ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then do
ffilister <- peek $ dataPtr ffiResult
lister <- Lister <$> newForeignPtr c_free_lister ffilister
return $ Right lister
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
-- | List dir in flat way.
-- Also, this function can be used to list a prefix.
-- An error will be returned if given path doesn’t end with /.
scanOpRaw :: Operator -> String -> IO (Either OpenDALError Lister)
scanOpRaw (Operator op) path = withForeignPtr op $ \opptr ->
withCString path $ \cPath ->
alloca $ \ffiResultPtr -> do
c_blocking_scan opptr cPath ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then do
ffilister <- peek $ dataPtr ffiResult
lister <- Lister <$> newForeignPtr c_free_lister ffilister
return $ Right lister
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg
-- | Get next entry path from `Lister`.
nextLister :: Lister -> IO (Either OpenDALError (Maybe String))
nextLister (Lister lister) = withForeignPtr lister $ \listerptr ->
alloca $ \ffiResultPtr -> do
c_lister_next listerptr ffiResultPtr
ffiResult <- peek ffiResultPtr
if ffiCode ffiResult == 0
then do
val <- peek $ dataPtr ffiResult
Right <$> parseCString val
else do
let code = parseErrorCode $ fromIntegral $ ffiCode ffiResult
errMsg <- peekCString (errorMessage ffiResult)
return $ Left $ OpenDALError code errMsg