{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveFoldable #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE TypeFamilies #-} -- | directory-layout internals module System.Directory.Layout.Internal where import Control.Applicative import Control.Lens import Control.Monad.Free import Data.ByteString (ByteString) import qualified Data.ByteString as ByteString import Data.Data (Data, Typeable) import Data.Foldable (Foldable) import qualified Data.HashMap.Strict as HashMap import Data.Semigroup (Semigroup(..)) import Data.String (IsString(..)) import Data.Word (Word8) import Data.Text (Text) #if __GLASGOW_HASKELL__ >= 708 import GHC.Exts (IsList(..)) #endif import GHC.Generics (Generic) import System.FilePath (()) import qualified System.Posix as Posix -- | Directory layout description newtype Layout a = L { unL :: Free F a } deriving (Functor, Applicative, Monad, Foldable, Traversable, Typeable, Generic) -- | The underlying 'Functor' data F a = F String (Maybe Contents) Aux a | SL String FilePath Bool Aux a | D String a Aux a | E deriving (Eq, Functor, Foldable, Traversable, Typeable, Generic) -- | Regular file contents data Contents = Binary ByteString | Text Text | CopyOf FilePath deriving (Eq, Typeable, Data, Generic) instance IsString Contents where fromString = Text . fromString #if __GLASGOW_HASKELL__ >= 708 instance IsList Contents where type Item Contents = Word8 fromList = Binary . ByteString.pack toList = error "Contents.toList: not implemented" #endif -- | Auxiliary data data Aux = Aux (Maybe User) (Maybe Group) (Maybe Posix.FileMode) deriving (Show, Eq, Typeable, Generic) -- | File owner data User = UserID Posix.UserID | Username String deriving (Show, Eq, Typeable, Generic) instance IsString User where fromString = Username -- | File group data Group = GroupID Posix.GroupID | Groupname String deriving (Show, Eq, Typeable, Generic) instance IsString Group where fromString = Groupname -- | Equality check does not care about the order the files are listed insofar -- they are consistent, i.e. different things aren't named the same instance Eq (Layout a) where L xs == L ys = go "." xs == go "." ys where go root (Free t@(F n _ _ m)) = HashMap.singleton (root n) (() <$ t) <> go root m go root (Free t@(SL n _ _ _ m)) = HashMap.singleton (root n) (() <$ t) <> go root m go root (Free t@(D n is _ m)) = HashMap.singleton (root n) (() <$ t) <> go (root n) is <> go root m go _ (Free E) = HashMap.empty go _ (Pure _) = HashMap.empty instance Semigroup (Layout a) where (<>) = (>>) -- | Regular file with some contents or empty -- -- >>> let layout = file "foo" file :: String -> Layout () file name = L (liftF (F name anything defaux ())) -- | Symbolic link -- -- >>> let layout = symlink "foo" "bar" symlink :: String -- link's name -> FilePath -- link's source -> Layout () symlink name s = L (liftF (SL name s False defaux ())) -- | Directory -- -- >>> :{ -- let layout = dir "foo" $ do -- file "bar" -- file "baz" -- :} dir :: String -> Layout a -> Layout () dir name is = L (Free (D name (unL is >> liftF E) defaux (Pure ()))) -- | Empty directory -- -- >>> let layout = emptydir "foo" emptydir :: String -> Layout () emptydir name = dir name (return ()) -- | A nested list of directories -- -- >>> :{ -- let layout = dirs ["foo", "bar"] $ do -- file "qux" -- file "quux" -- :} dirs :: [String] -> Layout () -> Layout () dirs names l = foldr dir l names -- | The default (empty) auxiliary data defaux :: Aux defaux = Aux Nothing Nothing Nothing -- | An optic into file contents contents :: Traversal' (Layout a) (Maybe Contents) contents f (L (Free (F n cs a x@(Pure _)))) = f cs <&> \cs' -> L (Free (F n cs' a x)) contents _ l = pure l {-# INLINE contents #-} -- | Binary contents -- -- >>> let layout = file "foo" & contents ?~ binary (ByteString.pack [1..10]) binary :: ByteString -> Contents binary = Binary -- | Plain text contents -- -- >>> let layout = file "foo" & contents ?~ text (Data.Text.pack "hello") text :: Text -> Contents text = Text -- | Contents are the copy of whose of the real file -- -- >>> let layout = file "foo" & contents ?~ copyOf "/home/user/.vimrc" copyOf :: FilePath -> Contents copyOf = CopyOf -- | Anything -- -- >>> let layout = file "foo" & contents .~ anything -- >>> let layout = file "foo" & user .~ anything anything :: Maybe a anything = Nothing -- | An optic into symbolic link source -- -- >>> symlink "foo" "bar" ^? source -- Just "bar" source :: Traversal' (Layout a) String source f (L (Free (SL n s e a x@(Pure _)))) = f s <&> \s' -> L (Free (SL n s' e a x)) source _ l = pure l {-# INLINE source #-} -- | An optic into symbolic link source expected existence -- -- >>> let layout = symlink "foo" "bar" & exists .~ True exists :: Traversal' (Layout a) Bool exists f (L (Free (SL n s e a x@(Pure _)))) = f e <&> \e' -> L (Free (SL n s e' a x)) exists _ l = pure l {-# INLINE exists #-} -- | An optic into file auxiliary data aux :: Traversal' (Layout a) Aux aux f (L (Free (F n cs a x@(Pure _)))) = f a <&> \a' -> L (Free (F n cs a' x)) aux f (L (Free (SL n s e a x@(Pure _)))) = f a <&> \a' -> L (Free (SL n s e a' x)) aux f (L (Free (D n is a x@(Pure _)))) = f a <&> \a' -> L (Free (D n is a' x)) aux _ l = pure l {-# INLINE aux #-} -- | An optic into file owner -- -- >>> let layout = file "foo" & user ?~ uid 0 user :: Traversal' (Layout a) (Maybe User) user = aux . \f (Aux x y z) -> f x <&> \x' -> Aux x' y z -- | Set the file owner by uid uid :: Posix.UserID -> User uid = UserID -- | Set the file owner by username -- -- >>> let layout = file "foo" & user ?~ username "root" username :: String -> User username = Username -- | An optic into file group -- -- >>> let layout = file "foo" & group ?~ gid 0 group :: Traversal' (Layout a) (Maybe Group) group = aux . \f (Aux x y z) -> f y <&> \y' -> Aux x y' z -- | Set the file group by groupname gid :: Posix.GroupID -> Group gid = GroupID -- | Set the file group by groupname -- -- >>> let layout = file "foo" & group ?~ groupname "wheel" groupname :: String -> Group groupname = Groupname -- | An optic into file mode -- -- >>> let layout = file "foo" & mode ?~ 0o100777 mode :: Traversal' (Layout a) (Maybe Posix.FileMode) mode = aux . \f (Aux x y z) -> f z <&> \z' -> Aux x y z' -- | An optic into directory contents innards :: Traversal' (Layout a) (Layout a) innards f (L (Free (D n is a x@(Pure _)))) = fmap unL (f (L is)) <&> \is' -> L (Free (D n is' a x)) innards _ l = pure l {-# INLINE innards #-} -- | An optic into the directory contents of the particular directory -- -- >>> :{ -- dirs ["foo", "bar", "baz"] (symlink "qux" "quux") -- ^? into "foo".into "bar".into "baz".focus "qux".source -- :} -- Just "quux" into :: String -> Traversal' (Layout ()) (Layout ()) into s = focus s.innards -- | An optic into the particular node focus :: String -> Traversal' (Layout ()) (Layout ()) focus k f = fmap L . go . unL where go (Free (F n cs a x)) | n == k = g (liftF (F n cs a ())) <&> \(Free (F _ cs' a' _)) -> Free (F n cs' a' x) | otherwise = go x <&> Free . F n cs a go (Free (D n is a x)) | n == k = g (Free (D n is a (Pure ()))) <&> \(Free (D _ is' a' _)) -> Free (D n is' a' x) | otherwise = go x <&> Free . D n is a go (Free (SL n s e a x)) | n == k = g (liftF (SL n s e a ())) <&> \(Free (SL _ s' e' a' _)) -> Free (SL n s' e' a' x) | otherwise = go x <&> Free . SL n s e a go (Free E) = pure (Free E) go (Pure x) = pure (Pure x) g = fmap unL . f . L {-# INLINE focus #-}