postgresql-lo-stream-0.1.1.0: src/Database/PostgreSQL/Simple/LargeObjects/Stream.hs
{-# LANGUAGE LambdaCase #-}
module Database.PostgreSQL.Simple.LargeObjects.Stream where
import Control.Exception.Lifted (AssertionFailed (..), bracket, throwIO)
import Control.Monad.Loops (whileJust_)
import Control.Monad.State as State
import qualified Data.ByteString as BS
import Data.ByteString.Builder (byteString)
import qualified Data.ByteString.Builder as BS
import qualified Data.ByteString.Lazy as LBS
import Data.IORef (modifyIORef, newIORef, readIORef)
import Data.Semigroup ((<>))
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Simple.LargeObjects (LoFd, Oid (..))
import qualified Database.PostgreSQL.Simple.LargeObjects as Sql
import System.IO (IOMode (ReadMode, WriteMode))
import System.IO.Streams (makeOutputStream)
import qualified System.IO.Streams as Streams
-- | Given a strict ByteString, create a postgres large object and fill it with those contents.
newLargeObjectBS
:: Connection
-> BS.ByteString
-> IO Oid
newLargeObjectBS conn contents = do
oid <- Sql.loCreat conn
n <- withLargeObject conn oid WriteMode $ \lofd -> Sql.loWrite conn lofd contents
let l = BS.length contents
when (n /= l) . throwIO . AssertionFailed $
"newLargeObjectBS: loWrite reported writing " <> show n <> " bytes, expected " <> show l <> "."
return oid
-- | Given a lazy ByteString, create a postgres large object and fill it with those contents.
-- Also returns the total length of the data written.
newLargeObjectLBS
:: Connection
-> LBS.ByteString
-> IO (Oid, Int)
newLargeObjectLBS conn = newLargeObjectStream conn <=< Streams.fromLazyByteString
-- | Create a new large object from an input stream, returning its object id and overall size.
newLargeObjectStream
:: Connection
-> Streams.InputStream BS.ByteString
-> IO (Oid, Int)
newLargeObjectStream conn s = do
oid <- Sql.loCreat conn
t <- withLargeObject conn oid WriteMode $ \lofd -> do
whileJust_ (Streams.read s) $ \chunk -> do
n <- Sql.loWrite conn lofd chunk
let l = BS.length chunk
when (n /= l) . throwIO . AssertionFailed $
"newLargeObjectStream: loWrite reported writing " <> show n <> " bytes, expected " <> show l <> "."
Sql.loTell conn lofd
return (oid, t)
-- | Act on a large object given by id, opening and closing the file descriptor appropriately.
withLargeObject
:: Connection
-> Oid
-> IOMode
-> (LoFd -> IO a)
-> IO a
withLargeObject conn oid mode f =
bracket (Sql.loOpen conn oid mode)
(\lofd -> Sql.loClose conn lofd)
f
-- | Stream the contents of a database large object to the given output stream. Useful with Snap's 'addToOutput'.
streamLargeObject
:: Connection
-> Oid
-> Streams.OutputStream BS.Builder
-> IO ()
streamLargeObject conn oid os =
withLargeObject conn oid ReadMode $ \lofd ->
fix $ \again -> do
chunk <- Sql.loRead conn lofd 8192 -- somewhat arbitrary
case BS.length chunk of
0 -> return ()
_ -> do
Streams.write (Just $ byteString chunk) os
again
-- | Stream the contents of a database large object to the given output stream. Useful with Snap's 'addToOutput'.
streamLargeObjectRange
:: Connection
-> Oid
-> Int
-> Int
-> Streams.OutputStream BS.Builder
-> IO ()
streamLargeObjectRange conn oid start end os =
withLargeObject conn oid ReadMode $ \lofd -> do
_ <- Sql.loSeek conn lofd Sql.AbsoluteSeek start
let again n = do
let nextChunkSize = min 8192 (end - n)
chunk <- Sql.loRead conn lofd nextChunkSize
case BS.length chunk of
0 -> return ()
k -> do
Streams.write (Just $ byteString chunk) os
again (n + k)
again start
-- | Act on the contents of a LargeObject as a Lazy ByteString
withLargeObjectLBS :: Connection -> Oid -> (LBS.ByteString -> IO ()) -> IO ()
withLargeObjectLBS conn oid f = do
lo <- newIORef mempty
cb <- makeOutputStream $ \case
Just chunk -> modifyIORef lo $ \chunks -> chunks <> chunk
Nothing -> do
payload <- readIORef lo
f $ BS.toLazyByteString payload
streamLargeObject conn oid cb
Streams.write Nothing cb