packages feed

conduit-vfs-0.1.0.0: src/Data/Conduit/VFS/Disk.hs

{-|
Description: VFS interface to the local filesystem (conventionally but increasingly wrongly called "writing to disk").

Read operations are not atomic, but write operations are probably atomic. Specifically, reads acquire a 'SharedLock' via 'hLock'. Writes first persist to a
temporary file, and then perform a copy using 'copyFileWithMetadata'. If 'copyFileWithMetadata' is atomic on your implementation (it probably is), then
writes are atomic.

The 'FilePath' values used in this VFS are split using 'splitPath' and joined using '</>'. Relative paths are resolved relative to the current working
directory: changing that directory is outside the scope of this module.

-}
module Data.Conduit.VFS.Disk
	( DiskVFS
	, runDiskVFS
	, runDiskVFS_
	) where


import ClassyPrelude hiding (ByteString, handle, hash, bracket)
import Control.Monad.Extra (ifM)
import Control.Monad.Fail (MonadFail)
import Control.Monad.Loops (whileM_)
import Data.Conduit.VFS.Import
import System.IO.Extra (openBinaryFile)
import System.Posix (getFileStatus, isRegularFile, isDirectory)
import System.Directory (removeFile)
import UnliftIO.Directory (doesFileExist, listDirectory)
import qualified Data.ByteString as SBS
import qualified Data.ByteString.Lazy as LBS

-- | The basic implementation of the VFS.
newtype DiskVFS m a = DiskVFS { unDVFS :: m a }
	deriving (Applicative, Functor, MonadFail, Monad)

instance (MonadIO m) => MonadIO (DiskVFS m) where
	liftIO = DiskVFS . liftIO
	{-# INLINE liftIO #-}

instance (MonadUnliftIO m) => MonadUnliftIO (DiskVFS m) where
	askUnliftIO = do
		(UnliftIO interiorUnliftIO) <- lift askUnliftIO
		return $ UnliftIO $ \(DiskVFS interior) -> interiorUnliftIO interior
	{-# INLINE askUnliftIO #-}

instance MonadTrans DiskVFS where
	lift = DiskVFS
	{-# INLINE lift #-}

-- | Given a 'DiskVFS', run it in the local monad and return the monadic return value.
runDiskVFS :: DiskVFS m a -> m a
runDiskVFS = unDVFS
{-# INLINE runDiskVFS #-}

-- | Given a 'DiskVFS', run it in the local monad and disregard any results.
runDiskVFS_ :: (Monad m) => DiskVFS m a -> m ()
runDiskVFS_ = void . runDiskVFS
{-# INLINE runDiskVFS_ #-}

instance (MonadUnliftIO m) => ReadVFSC (DiskVFS m) where

	vfsTypeC = awaitForever $ \filepath -> fmap (filepath,) . liftIO $
		ifM
			(not <$> doesFileExist filepath)
			(return Nothing)
			$ getFileStatus filepath >>= \status ->
				if isRegularFile status then
					return $ Just VFile
				else if isDirectory status then
					return $ Just VDirectory
				else
					return Nothing
	{-# INLINEABLE vfsTypeC #-}

	vfsContentsEitherC = awaitForever $ \filepath ->
			whenM (isExistingRegularFile filepath)
				$ do
					yield $ Left filepath
					handle <- liftIO $ openBinaryFile filepath ReadMode
					liftIO $ hSetBuffering handle (BlockBuffering Nothing)
					whileM_
						(hIsNotEOF handle)
						(doRead handle >>= yield . Right . LBS.fromStrict)
		where
			hIsNotEOF handle = liftIO $ not <$> hIsEOF handle
			doRead h = liftIO $ SBS.hGetSome h 1024
	{-# INLINEABLE vfsContentsEitherC #-}

	vfsChildrenC = awaitForever $ \filepath ->
			whenM (liftIO $ doesFileExist filepath) $
				ifM
					(fileIsDirectory filepath)
					(listChildren filepath >>= yieldMany)
					(yield filepath)
		where
			fileIsDirectory filepath = liftIO $ isDirectory <$> getFileStatus filepath
			listChildren filepath = liftIO $ do
				(children::[FilePath]) <- listDirectory filepath
				return $ (filepath </>) <$> children
	{-# INLINEABLE vfsChildrenC #-}

-- | A class denoting that the type is usable as VFS conduits for writing.
instance (MonadUnliftIO m) => WriteVFSC (DiskVFS m) where

	vfsWriteEitherSink = awaitForever $ \case
			(Right _) ->  fail "Encountered bytes without seeing a filename"
			(Left filename) -> do
				bytes <- readAllBytesFromUpstream
				liftIO $ LBS.writeFile filename bytes
		where
			readAllBytesFromUpstream =
				ifM
					moreBytesFromUpstream
					(readSomeBytesFromUpstream >>= \prev -> LBS.append prev <$> readAllBytesFromUpstream )
					(return mempty)
			moreBytesFromUpstream = peekC >>= \case
					(Just (Right _)) -> return True
					_ -> return False
			readSomeBytesFromUpstream = await >>= \case
					(Just (Right bytes)) -> return bytes
					_ -> fail "Encountered a new filename when peeking said we had bytes"
	{-# INLINEABLE vfsWriteEitherSink #-}

	vfsRemoveSink = awaitForever $ \filename ->
			whenM (isExistingRegularFile filename) (liftIO $ removeFile filename)
	{-# INLINE vfsRemoveSink #-}

isExistingRegularFile :: MonadIO m => FilePath -> m Bool
isExistingRegularFile filepath = liftIO $ liftM2 (&&) (doesFileExist filepath) (isRegularFile <$> getFileStatus filepath)

-- | A class denoting that the type is usable as VFS conduits for reading and writing.
instance (MonadUnliftIO m) => VFSC (DiskVFS m)