directory-layout-0.7.0.0: src/System/Directory/Layout/Interpreter.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-}
-- | A bunch of 'Layout' description interpreters
module System.Directory.Layout.Interpreter
( pretty
, examples
, fromErrors
, fit
, FitError(..)
, FitContentsError(..)
, make
, remake
, MakeError(..)
#ifdef TEST
, (\/)(..)
#endif
) where
import Control.Applicative
import Control.Exception (Exception(..), SomeException(..), throwIO, try)
import Control.Monad
import Control.Monad.Free
import Data.ByteString (ByteString)
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Lazy as ByteStringLazy
import Data.Data (Data, Typeable)
import Data.Foldable (Foldable, sequenceA_, for_, toList)
import Data.Functor.Compose (Compose(..))
import Data.List.NonEmpty (NonEmpty(..))
import Data.Maybe (fromMaybe)
import Data.Semigroup (Semigroup(..))
import Data.Text (Text)
import qualified Data.Text.IO as Text
import Data.Traversable (Traversable)
import Data.Typeable (cast)
import GHC.Generics (Generic)
import Numeric (showOct)
import System.Directory (createDirectory, createDirectoryIfMissing, removeDirectoryRecursive)
import System.FilePath (combine)
import System.IO.Error (IOErrorType, ioeGetErrorType, ioeGetFileName, ioeGetLocation)
import qualified System.Posix as Posix
import Test.Hspec (Spec, context, it)
import Text.Printf (printf)
import System.Directory.Layout.Internal
-- | Pretty print the directory layout
pretty :: Layout a -> String
pretty = unlines . iter go . unL . fmap (const []) where
go f@(F _ _ _ other) = prettyF f : other
go f@(SL _ _ _ _ other) = prettyF f : other
go f@(D _ is _ other) = prettyF f : map indent is ++ other
go E = []
indent :: String -> String
indent s = "┆ " ++ s
prettyF :: F a -> String
prettyF (F name cs _ _) = printf "‘%s’, %s" name (prettyC cs)
prettyF (SL name s _ _ _) = printf "‘%s’, a link to ‘%s’" name s
prettyF (D name _ _ _) = '/' : name
prettyF E = ""
prettyC :: Maybe Contents -> String
prettyC (Just (Binary _)) = "raw bytes"
prettyC (Just (Text _)) = "text"
prettyC (Just (CopyOf p)) = printf "(copy of ‘%s’)" p
prettyC Nothing = "anything"
-- | Interpret the directory layout as a 'Spec'
examples :: FilePath -> Layout a -> Spec
examples p = go p . unL where
go root (Free f@(F _ _ _ m)) = do
examplesF root f
go root m
go root (Free f@(SL _ _ _ _ m)) = do
examplesF root f
go root m
go root (Free f@(D (combine root -> fullpath) is _ m)) = do
examplesF root f
context (printf "directory ‘%s’" fullpath) (go fullpath is)
go root m
go _ (Free E) = return ()
go _ (Pure _) = return ()
examplesF :: FilePath -> F a -> Spec
examplesF root = go where
go f@(F name cs _ _) = it (printf "has a %s file ‘%s’" (examplesC cs) name) (fitIO root f)
go f@(SL name s _ _ _) = it (printf "has a symlink ‘%s’ pointing to ‘%s’" name s) (fitIO root f)
go f@(D (combine root -> fullpath) _ _ _) = it (printf "has a subdirectory ‘%s’" fullpath) (fitIO root f)
go E = return ()
examplesC :: Maybe Contents -> String
examplesC (Just (Binary _)) = "binary"
examplesC (Just (Text _)) = "plain text"
examplesC (Just (CopyOf p)) = printf "(copy of ‘%s’)" p
examplesC Nothing = "regular"
validate
:: Exception e
=> (forall a. FilePath -> F a -> IO ()) -> FilePath -> Layout b -> IO (NonEmpty e \/ ())
validate g p = getCompose . go p . unL where
go root (Free f@(F _ _ _ m)) =
sequenceA_ [Compose (validateF root f), go root m]
go root (Free f@(SL _ _ _ _ m)) =
sequenceA_ [Compose (validateF root f), go root m]
go root (Free f@(D (combine root -> fullpath) is _ m)) =
sequenceA_ [Compose (validateF root f), go fullpath is, go root m]
go _ (Free E) = pure ()
go _ (Pure _) = pure ()
validateF root = validateIO . g root
validateIO :: Exception e => IO a -> IO (NonEmpty e \/ a)
validateIO io = either (Error . pure) Result <$> try io
-- | Check the real directory layout fits the description
fit :: FilePath -> Layout a -> IO (Either (NonEmpty FitError) ())
fit p = fmap toEither . validate fitIO p
fitIO :: FilePath -> F a -> IO ()
fitIO root = go where
go (F (combine root -> fullpath) cs a _) = do
for_ cs $ \cs' -> case cs' of
Binary bs -> do
real <- ByteString.readFile fullpath
when (real /= bs) $
throwIO (FitBadFileContents fullpath (FitBadBinary bs real))
Text t -> do
real <- Text.readFile fullpath
when (real /= t) $
throwIO (FitBadFileContents fullpath (FitBadText t real))
CopyOf f -> do
origin <- ByteStringLazy.readFile f
copy <- ByteStringLazy.readFile fullpath
when (origin /= copy) $
throwIO (FitBadFileContents fullpath (FitBadCopyOf f))
fitIOAux a fullpath
go (SL (combine root -> fullpath) s e a _) = do
path <- Posix.readSymbolicLink fullpath
when (path /= s) $
throwIO (FitBadLinkSource fullpath s path)
when e $
() <$ Posix.getFileStatus fullpath
fitIOAux a fullpath
go (D (combine root -> fullpath) _ a _) = () <$ do
fitIOAux a fullpath
go E = return ()
fitIOAux :: Aux -> FilePath -> IO ()
fitIOAux (Aux muid mgid mperm) path = do
status <- Posix.getSymbolicLinkStatus path
for_ muid $ \case
UserID i ->
unless (Posix.fileOwner status == i) $
throwIO (FitBadOwnerUser path (UserID i) (UserID (Posix.fileOwner status)))
Username name -> do
i <- getUserID name
n <- getUsername (Posix.fileOwner status)
unless (Posix.fileOwner status == i) $
throwIO (FitBadOwnerUser path (Username name) (Username n))
for_ mgid $ \case
GroupID i ->
unless (Posix.fileGroup status == i) $
throwIO (FitBadOwnerGroup path (GroupID i) (GroupID (Posix.fileGroup status)))
Groupname name -> do
i <- getGroupID name
n <- getGroupname (Posix.fileGroup status)
unless (Posix.fileGroup status == i) $
throwIO (FitBadOwnerGroup path (Groupname name) (Groupname n))
for_ mperm $ \perm ->
unless (Posix.fileMode status == perm) $
throwIO (FitBadFileMode path perm (Posix.fileMode status))
-- | Errors encountered while running 'fit'
data FitError =
FitBadFileContents FilePath FitContentsError
| FitBadLinkSource FilePath String {- expected -} String {- actual -}
| FitBadOwnerUser FilePath User {- expected -} User {- actual -}
| FitBadOwnerGroup FilePath Group {- expected -} Group {- actual -}
| FitBadFileMode FilePath Posix.FileMode {- expected -} Posix.FileMode {- actual -}
| FitIOException FilePath IOErrorType
deriving (Eq, Typeable, Generic)
-- | Expected/actual file contents mismatch
data FitContentsError =
FitBadBinary ByteString ByteString
| FitBadText Text Text
| FitBadCopyOf FilePath
deriving (Eq, Typeable, Generic)
instance Show FitError where
show (FitBadFileContents path mismatch) = unlines $
printf "Bad contents at ‘%s’" path : showCE mismatch
where
showCE :: FitContentsError -> [String]
showCE (FitBadBinary expected actual) =
[ "expected:"
, printf " %s" (show (ByteString.unpack expected))
, "actual:"
, printf " %s" (show (ByteString.unpack actual))
]
showCE (FitBadText expected actual) =
[ "expected:"
, printf " %s" (show expected)
, "actual:"
, printf " %s" (show actual)
]
showCE (FitBadCopyOf f) =
[ "expected:"
, printf " a copy of ‘%s’" f
, "actual:"
, " something else"
]
show (FitBadLinkSource path expected actual) = unlines $
[ printf "Bad symlink source at ‘%s’" path
, "expected:"
, printf " ‘%s’" expected
, "actual:"
, printf " ‘%s’" actual
]
show (FitBadOwnerUser path expected actual) = unlines $
[ printf "Bad owner user id at ‘%s’" path
, "expected:"
, printf " %s" (show expected)
, "actual:"
, printf " %s" (show actual)
]
show (FitBadOwnerGroup path expected actual) = unlines $
[ printf "Bad owner group id at ‘%s’" path
, "expected:"
, printf " %s" (show expected)
, "actual:"
, printf " %s" (show actual)
]
show (FitBadFileMode path expected actual) = unlines $
[ printf "Bad file permissions id at ‘%s’" path
, "expected:"
, printf " %s" (showOct expected "")
, "actual:"
, printf " %s" (showOct actual "")
]
show (FitIOException eloc etype) =
printf "Generic IO exception of type ‘%s’ happened at ‘%s’\n" (show etype) eloc
instance Exception FitError where
toException = SomeException
fromException e'@(SomeException e)
| Just ioe <- fromException e' =
Just (FitIOException (fromMaybe (ioeGetLocation ioe) (ioeGetFileName ioe)) (ioeGetErrorType ioe))
| otherwise = cast e
-- | Make the real directory layout from the description
make :: FilePath -> Layout a -> IO (Either (NonEmpty MakeError) ())
make p = fmap toEither . validate makeIO p
-- | Make the real directory layout from the description removing any previous contents
remake :: FilePath -> Layout a -> IO (Either (NonEmpty MakeError) ())
remake p l = fmap toEither . getCompose $
Compose (validateIO (removeDirectoryRecursive p *> createDirectory p)) *>
Compose (fmap fromEither (make p l))
makeIO :: FilePath -> F a -> IO ()
makeIO root = go where
go (F (combine root -> fullpath) cs a _) = do
case cs of
Just (Binary bs) -> ByteString.writeFile fullpath bs
Just (Text t) -> Text.writeFile fullpath t
Just (CopyOf p) -> ByteStringLazy.readFile p >>= ByteStringLazy.writeFile fullpath
Nothing -> ByteString.writeFile fullpath (ByteString.pack [])
makeIOAux a fullpath
go (SL (combine root -> fullpath) s _ a _) = do
Posix.createSymbolicLink s fullpath
makeIOAux a fullpath
go (D (combine root -> fullpath) _ a _) = do
createDirectoryIfMissing False fullpath
makeIOAux a fullpath
go E = return ()
makeIOAux :: Aux -> FilePath -> IO ()
makeIOAux (Aux muid mgid mperm) path = do
for_ muid $ \case
UserID i ->
Posix.setSymbolicLinkOwnerAndGroup path i (-1)
Username name -> do
i <- getUserID name
Posix.setSymbolicLinkOwnerAndGroup path i (-1)
for_ mgid $ \case
GroupID i ->
Posix.setSymbolicLinkOwnerAndGroup path (-1) i
Groupname name -> do
i <- getGroupID name
Posix.setSymbolicLinkOwnerAndGroup path (-1) i
for_ mperm $
Posix.setFileMode path
-- | Errors encountered while running 'make'
data MakeError =
MakeIOException FilePath IOErrorType
deriving (Show, Eq, Typeable, Generic)
instance Exception MakeError where
toException = SomeException
fromException e'@(SomeException e)
| Just ioe <- fromException e' =
Just (MakeIOException (fromMaybe (ioeGetLocation ioe) (ioeGetFileName ioe)) (ioeGetErrorType ioe))
| otherwise = cast e
getUserID :: String -> IO Posix.UserID
getUserID = fmap Posix.userID . Posix.getUserEntryForName
getUsername :: Posix.UserID -> IO String
getUsername = fmap Posix.userName . Posix.getUserEntryForID
getGroupID :: String -> IO Posix.GroupID
getGroupID = fmap Posix.groupID . Posix.getGroupEntryForName
getGroupname :: Posix.GroupID -> IO String
getGroupname = fmap Posix.groupName . Posix.getGroupEntryForID
-- | This type is isomorphic to 'Either' but its 'Applicative' instance accumulates errors
data e \/ a = Error e | Result a
deriving (Show, Eq, Ord, Functor, Foldable, Traversable, Typeable, Data, Generic)
instance Semigroup e => Applicative ((\/) e) where
pure = Result
Error f <*> Error x = Error (f <> x)
Error f <*> _ = Error f
_ <*> Error x = Error x
Result f <*> Result x = Result (f x)
fromEither :: Either e a -> e \/ a
fromEither = either Error Result
toEither :: e \/ a -> Either e a
toEither = validation Left Right
validation :: (e -> t) -> (a -> t) -> e \/ a -> t
validation f _ (Error e) = f e
validation _ g (Result a) = g a
-- | Construct 'Validation' value from the list of errors
--
-- >>> fromErrors []
-- Right ()
--
-- >>> fromErrors Nothing
-- Right ()
--
-- >>> fromErrors "hello"
-- Left ('h' :| "ello")
--
-- >>> fromErrors (Just "hello")
-- Left ("hello" :| [])
fromErrors :: Foldable t => t e -> Either (NonEmpty e) ()
fromErrors = go . toList
where
go [] = Right ()
go (x : xs) = Left (x :| xs)