packages feed

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

{-|

Description: The types that make up this library.

-}

module Data.Conduit.VFS.Types
( FilePath
, ConduitT
, VFSSource
, VFSPipe
, VFSSink
, VFileType(..)
, ReadVFSC(..)
, WriteVFSC(..)
, VFSC(..)
, ByteString
) where

import ClassyPrelude hiding (ByteString)
import System.FilePath (FilePath, takeDirectory)
import Conduit
import qualified Data.ByteString.Lazy as LBS
import Data.Either (fromRight)
import Control.Monad.Extra (ifM)

type ByteString = LBS.ByteString

-- | The type of conduits that generate file paths.
type VFSSource m = ConduitT Void FilePath m ()

-- | The type of conduits that consume file paths and generate file paths.
type VFSPipe m = ConduitT FilePath FilePath m ()

-- | The type of conduits that consume file paths.
type VFSSink m r = ConduitT FilePath Void m r

-- | The types that our virtual file system supports.
data VFileType
	= VFile        -- ^ A node containing bytes
	| VDirectory   -- ^ A node containing zero or more other nodes
	deriving (Eq, Ord, Show, Generic, Typeable, Enum, Bounded)

-- | A class denoting that the type is usable as VFS conduits for reading.
class (Monad m) => ReadVFSC m where

	{-# MINIMAL ( vfsContentsEitherC | vfsContentsC ), vfsTypeC, ( vfsChildrenC | vfsDescendentsC ) #-}

	-- | Given an input path, generates a tuple of the input path itself and the input path's 'VFileType' (or 'Nothing' if the node does not exist).
	--   Note that a directory containing no elements may be reported by the VFS as not existing.
	vfsTypeC :: ConduitT FilePath (FilePath, Maybe VFileType) m ()

	-- | Given an input of 'FilePath' files, generates a tuple containing the input and the bytestring for the contents of the file.
	--   Note that the entire contents of the file are pulled into memory.  If the input 'FilePath' does not denote a 'VFile', it should be dropped.
	vfsContentsC :: ConduitT FilePath (FilePath, LBS.ByteString) m ()
	vfsContentsC = awaitForever $ \path -> do
			bytes <- yield path .| vfsContentsEitherC .| mapC (fromRight mempty) .| foldC
			yield (path, bytes)
	{-# INLINEABLE vfsContentsC #-}

	-- | Given an input of 'FilePath' files, generates a 'Left' of the input, followed by zero or more 'Right' values holding a bytestring. The concatenation of the
	--   'Right' values after a given 'Left' and before the next 'Left' (or EOS) are the bytes of the input value. If the input 'FilePath' does not denote a 'VFile',
	--   it should be dropped.
	vfsContentsEitherC :: ConduitT FilePath (Either FilePath LBS.ByteString) m ()
	vfsContentsEitherC = awaitForever $ \path -> do
			yield $ Left path
			bytes <- yield path .| vfsContentsC .| mapC snd .| foldC
			yield $ Right bytes
	{-# INLINEABLE vfsContentsEitherC #-}

	-- | Given an input of 'FilePath' directories, generates the non-special direct children, each path-prepended (using '</>') with the parent directory.
	--   If an input 'FilePath' is not a 'VDirectory', it should be passed through directly.
	vfsChildrenC :: VFSPipe m
	vfsChildrenC = awaitForever $ \path -> do
			children <- yield path .| vfsDescendentsC .| filterC (\it -> path == takeDirectory it ) .| sinkList -- TODO: Is this a bug if we encounter paths like /foo/bar/foo?
			yieldMany children
	{-# INLINEABLE vfsChildrenC #-}

	-- | Given an input of 'FilePath' directories, generates the non-special direct children that are files, each path-prepended (using '</>') with the
	--   parent directory.  If an input 'FilePath' is not a 'VDirectory', it should be passed through directly.
	vfsChildFilesC :: VFSPipe m
	vfsChildFilesC = vfsChildrenC .| vfsTypeC .| filterC ( (Just VFile ==) . snd ) .| mapC fst
	{-# INLINE vfsChildFilesC #-}

	-- | Given an input of 'FilePath' directories, generates the non-special direct children that are files, each path-prepended (using '</>') with the
	--   parent directory.  If an input 'FilePath' is not a 'VDirectory', it should be dropped.
	vfsChildDirsC :: VFSPipe m
	vfsChildDirsC = vfsChildrenC .| vfsTypeC .| filterC ( (Just VDirectory ==) . snd ) .| mapC fst
	{-# INLINE vfsChildDirsC #-}

	-- | Given an input of 'FilePath' directories, generates all the paths in the VFS that have the input as a prefix, with the outputs being each
	--   path-prepended (using '</>') with the corresponding input directory. If an  input 'FilePath' is not a 'VDirectory', it should be passed through
	--   directly.
	vfsDescendentsC :: VFSPipe m
	vfsDescendentsC = awaitForever $ \path -> do
			yield path
			loop path
		where
			loop path = do
				children <- yield path .| vfsChildrenC .| sinkList
				yieldMany children
				unless (null children) (sequence_ $ loop <$> children)
	{-# INLINEABLE vfsDescendentsC #-}

	-- | Given an input 'FilePath' directories, generates all the paths in the VFS that are files and have the input as a prefix, with the outputs being
	--   each path-prepended (using '</>') with the corresponding input directory. If an input 'FilePath' is not a 'VDirectory', it should be passed through directly.
	vfsDescFilesC :: VFSPipe m
	vfsDescFilesC = vfsDescendentsC .| vfsTypeC .| filterC (\(_, maybeFileType) -> Just VFile == maybeFileType) .| mapC fst
	{-# INLINE vfsDescFilesC #-}

	-- | Given an input of 'FilePath' directories, generates all the paths in the VFS that are directories and have the input as a prefix, with the outputs being
	--   each path-prepended (using '</>') with the corresponding input directory. If an input 'FilePath' is not a 'VDirectory', it should be dropped.
	vfsDescDirsC :: VFSPipe m
	vfsDescDirsC = vfsDescendentsC .| vfsTypeC .| filterC ( (Just VDirectory ==) . snd ) .| mapC fst
	{-# INLINE vfsDescDirsC #-}

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

	{-# MINIMAL (vfsWriteSink | vfsWriteEitherSink), vfsRemoveSink #-}

	-- | Given an input tuple of 'FilePath' files and their bytestring contents, writes the contents to the filepath. This write should be atomic if possible, and if
	--   it is not an atomic operation, the implementation's documentation should make this clear. This write should also create any necessary directories that may
	--   not have previously existed.
	vfsWriteSink :: ConduitT (FilePath, LBS.ByteString) Void m ()
	vfsWriteSink = awaitForever $ \(filepath, bs) -> yieldMany [Left filepath, Right bs] .| vfsWriteEitherSink
	{-# INLINE vfsWriteSink #-}

	-- | Given an input of either 'FilePath' files or bytestring contents, writes the contents to the filepath. The write is marked as complete when the next
	--   'FilePath' input or end-of-stream is reached. This write should be atomic at completion if possible, and if it is not an atomic operation, the
	--   implementation's documentation should make this clear. This write should also create any necessary directories that may not have previously existed.
	vfsWriteEitherSink :: ConduitT (Either FilePath LBS.ByteString) Void m ()
	vfsWriteEitherSink = awaitForever $ \case
			(Right _) -> return () -- WTF?
			(Left filepath) -> do
				bytes <- bytesLoop
				yield (filepath, bytes) .| vfsWriteSink
		where
			bytesLoop = peekC >>= \case
				(Just (Right bytes)) -> await >> (bytes <>) <$> bytesLoop
				_                    -> return mempty
	{-# INLINEABLE vfsWriteEitherSink #-}

	-- | Given 'FilePath' inputs, remove those nodes from the VFS. If the path denotes a directory, the directory is
	--   removed along with all of its descendents. If the path denotes a file, the file itself is removed. After a removal,
	--   any newly-empty directories may also be removed.
	vfsRemoveSink :: VFSSink m ()

-- | A class denoting that the type is usable as VFS conduits for both reading and writing.
class (ReadVFSC m, WriteVFSC m) => VFSC m where

	-- | Given an input tuple of a filetype and a filepath, ensure that a node exists at the filepath. If it does not exist, it should be created as either
	--   a directory or a zero-length file, as denoted by the filetype, with any missing parent directories created.  Note that a directory which does not
	--   contain a node may be reported as not present by the VFS, and therefore an acceptable implementation for 'VDirectory' inputs is simply 'return ()'
	vfsTouchSink :: ConduitT (VFileType, FilePath) Void m ()
	vfsTouchSink = awaitForever $ \(filetype, filepath) ->
		ifM
			( yield filepath .| vfsTypeC .| headC >>= \case
				Nothing -> return False
				(Just (_, Nothing)) -> return False
				(Just (_, Just foundFileType)) -> return $ filetype == foundFileType
			)
			( case filetype of
					VFile -> yield (filepath, mempty) .| vfsWriteSink
					VDirectory -> return ()
			)
			( return () )
	{-# INLINEABLE vfsTouchSink #-}