packages feed

porcupine-s3-0.1.0.0: src/Data/Locations/Accessors/AWS.hs

{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveTraversable          #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE NamedFieldPuns             #-}
{-# LANGUAGE OverloadedLabels           #-}
{-# LANGUAGE PatternSynonyms            #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE TypeOperators              #-}
{-# OPTIONS_GHC "-fno-warn-orphans" #-}
{-# OPTIONS_GHC "-fno-warn-name-shadowing" #-}

module Data.Locations.Accessors.AWS
  ( module Control.Monad.ReaderSoup.AWS
  , runPipelineTaskS3
  -- * Backward-compat API
  , selectRun
  , runWriteLazyByte
  , runReadLazyByte
  , runReadLazyByte_
  ) where

import           Control.Exception.Safe
import           Control.Lens
import           Control.Monad.ReaderSoup
import           Control.Monad.ReaderSoup.AWS
import           Control.Monad.ReaderSoup.Katip   ()
import           Control.Monad.Trans.Resource
import qualified Data.ByteString.Lazy             as LBS
import qualified Data.ByteString.Streaming        as BSS
import           Data.Locations.Accessors
import           Data.Locations.Loc
import           Data.String
import           Network.AWS                      hiding (Error)
import           Network.AWS.S3
import qualified Network.AWS.S3.TaskPipelineUtils as S3
import           System.TaskPipeline.CLI
import           System.TaskPipeline.PTask
import           System.TaskPipeline.Run


-- | Just a compatiblity overlay for code explicitly dealing with S3 URLs
pattern S3Obj :: String -> PathWithExtension a -> URL a
pattern S3Obj{bucketName,objectName} = RemoteFile "s3" bucketName Nothing objectName []

-- | Accessing resources on S3
instance (MonadAWS m, MonadMask m, MonadResource m)
      => LocationAccessor m "aws" where
  newtype GLocOf "aws" a = S (URL a)
    deriving (Functor, Foldable, Traversable, ToJSON, TypedLocation)
  locExists _ = return True -- TODO: Implement it
  writeBSS (S l) = writeBSS_S3 l
  readBSS (S l) f = readBSS_S3 l f
  copy (S l1) (S l2) = copy_S3 l1 l2

instance (MonadAWS m, MonadMask m, MonadResource m)
      => MayProvideLocationAccessors m "aws"

instance (IsLocString a) => Show (GLocOf "aws" a) where
  show (S l) = show l

instance (IsLocString a) => FromJSON (GLocOf "aws" a) where
  parseJSON v = do
    loc <- parseJSON v
    case loc of
      S3Obj{} -> return $ S loc
      _       -> fail "Doesn't use 's3' protocol"

writeBSS_S3 :: MonadAWS m => Loc -> BSS.ByteString m a -> m a
writeBSS_S3 S3Obj { bucketName, objectName } body = do
  let raw = objectName ^. pathWithExtensionAsRawFilePath
  (res, r) <- S3.uploadObj (fromString bucketName) (fromString raw) body
  case res ^. porsResponseStatus of
    200 -> pure ()
    _   -> error $ "Unable to upload to the object " ++ raw ++ "."
  return r
writeBSS_S3 _ _ = undefined

readBSS_S3
  :: (MonadAWS m)
  => Loc
  -> (BSS.ByteString m () -> m b)
  -> m b
readBSS_S3 S3Obj{ bucketName, objectName } k = do
  r <- S3.streamObjInto
         (fromString bucketName)
         (fromString $ objectName ^. pathWithExtensionAsRawFilePath)
         k
  case r of
    Left e  -> throw e
    Right r -> return r
readBSS_S3 _ _ = undefined

copy_S3
  :: (MonadResource m, MonadAWS m)
  => Loc
  -> Loc
  -> m ()
copy_S3 locFrom@(S3Obj bucket1 obj1) locTo@(S3Obj bucket2 obj2)
  | bucket1 == bucket2 = do
      _ <- S3.copyObj
             (fromString bucket1)
             (fromString $ obj1^.pathWithExtensionAsRawFilePath)
             (fromString $ obj2^.pathWithExtensionAsRawFilePath)
      return ()
  | otherwise = readBSS_S3 locFrom (writeBSS_S3 locTo)
copy_S3 _ _ = undefined

-- | Just a shortcut for when you want ONLY local files and S3 support, with AWS
-- credentials discovery. Use 'runPipelineTask' if you want to activate other
-- location accessors.
runPipelineTaskS3
  :: PipelineConfigMethod o  -- ^ How to configure the pipeline
  -> Maybe Region            -- ^ Change the default AWS region
  -> PTask (ReaderSoup (("aws":::ContextFromName "aws") : BasePorcupineContexts)) () o
  -> IO o
runPipelineTaskS3 pcm mbRegion ptask =
  runPipelineTask pcm (  #aws <-- case mbRegion of
                                    Nothing  -> useAWS Discover
                                    Just reg -> useAWSRegion Discover reg
                      :& baseContexts (pcm ^. pipelineConfigMethodProgName) ) ptask ()


-- DEPRECATED CODE:

-- * Automatically switching from Resource to AWS monad when accessing some loc

-- | Run a computation or a sequence of computations that will access some
-- locations. Selects whether to run in IO or AWS based on some Loc used as
-- selector.
selectRun :: Loc  -- ^ A Loc to access
          -> (forall m l. (LocationAccessor m l) => LocOf l -> m a)
             -- ^ The action to run, either in AWS or IO
          -> IO a
selectRun loc f =
  case loc of
    LocalFile{} -> do
      let accessorsRec = baseContexts "selectRun_Local"
          (_,argsRec) = splitAccessorsFromArgRec accessorsRec
      consumeSoup argsRec $ f (L loc)
    S3Obj{} -> do
      let accessorsRec =    #aws <-- useAWS Discover
                         :& baseContexts "selectRun_AWS"
          (_,argsRec) = splitAccessorsFromArgRec accessorsRec
      consumeSoup argsRec $ f (S loc)
    _ -> error "selectRun only handles local and S3 locations"

-- | Just a shortcut
runWriteLazyByte
  :: Loc
  -> LBS.ByteString
  -> IO ()
runWriteLazyByte l bs = selectRun l $ \l' -> writeLazyByte l' bs

-- | Just a shortcut
runReadLazyByte, runReadLazyByte_ :: Loc -> IO LBS.ByteString
runReadLazyByte l = selectRun l readLazyByte
runReadLazyByte_ = runReadLazyByte