dirforest-0.1.0.0: src/Data/DirForest.hs
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | A directory forest of files associated with values
--
-- This module is meant to be imported like this:
--
-- > import Data.DirForest (DirForest)
-- > import qualified Data.DirForest as DF
module Data.DirForest
( -- * Dirforest types
DirTree (..),
DirForest (..),
InsertionError (..),
FOD (..),
-- * Comparisons
eq1DirTree,
ord1DirTree,
eq1DirForest,
ord1DirForest,
-- * Query
null,
nullFiles,
lookup,
-- * Construction
empty,
singletonFile,
singletonDir,
insertFile,
insertDir,
-- * Traversal
mapWithPath,
traverseWithPath,
traverseWithPath_,
-- * Pruning
pruneEmptyDirs,
anyEmptyDir,
-- * Conversion
-- ** Map
fromFileMap,
toFileMap,
fromMap,
toMap,
-- ** List
fromFileList,
toFileList,
-- * IO
-- ** Read
read,
readNonHidden,
readFiltered,
readNonHiddenFiltered,
readOneLevel,
readOneLevelNonHidden,
readOneLevelFiltered,
readOneLevelNonHiddenFiltered,
hiddenRel,
-- ** Write
write,
-- * Combinations
-- ** Union
InsertValidation (..),
unpackInsertValidation,
union,
unionWith,
unionWithKey,
unions,
-- ** Intersection
intersection,
intersectionWith,
intersectionWithKey,
intersections,
-- ** Difference
difference,
differenceWith,
differenceWithKey,
-- * Filter
filter,
filterWithKey,
filterHidden,
)
where
import Autodocodec
import Control.Arrow (left)
import Control.DeepSeq
import Control.Monad
import Control.Monad.IO.Class
import Data.Aeson (FromJSON, ToJSON)
import Data.Functor.Classes
import Data.Functor.Identity
import Data.List (foldl')
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import Data.Map (Map)
import qualified Data.Map as M
import Data.Maybe
import Data.Validity
import Data.Validity.Containers ()
import Data.Validity.Map
import Data.Validity.Path ()
import GHC.Generics (Generic)
import Path
import Path.IO
import Path.Internal
import qualified System.FilePath as FP
import Prelude hiding (filter, lookup, null, read)
import qualified Prelude
data DirTree a
= NodeFile a
| NodeDir (DirForest a)
deriving stock (Show, Generic, Functor)
deriving (FromJSON, ToJSON) via (Autodocodec (DirTree a))
instance (Validity a) => Validity (DirTree a)
instance Eq a => Eq (DirTree a) where
(==) = eq1DirTree (==)
instance Ord a => Ord (DirTree a) where
compare = ord1DirTree compare
instance Eq1 DirTree where
liftEq = eq1DirTree
instance Ord1 DirTree where
liftCompare = ord1DirTree
instance NFData a => NFData (DirTree a)
instance Foldable DirTree where
foldMap func =
\case
NodeFile v -> func v
NodeDir df -> foldMap func df
instance Traversable DirTree where
traverse func =
\case
NodeFile v -> NodeFile <$> func v
NodeDir df -> NodeDir <$> traverse func df
instance HasCodec a => HasCodec (DirTree a) where
codec =
named "DirTree" $
dimapCodec f g $
eitherCodec
(codec :: JSONCodec a)
(codec :: JSONCodec (DirForest a))
where
f = \case
Left a -> NodeFile a
Right b -> NodeDir b
g = \case
NodeFile a -> Left a
NodeDir b -> Right b
eq1DirTree :: (a -> b -> Bool) -> DirTree a -> DirTree b -> Bool
eq1DirTree eq dt1 dt2 = case (dt1, dt2) of
(NodeFile a1, NodeFile a2) -> eq a1 a2
(NodeDir df1, NodeDir df2) -> eq1DirForest eq df1 df2
_ -> False
ord1DirTree :: (a -> b -> Ordering) -> DirTree a -> DirTree b -> Ordering
ord1DirTree cmp dt1 dt2 = case (dt1, dt2) of
(NodeFile a1, NodeFile a2) -> cmp a1 a2
(NodeDir df1, NodeDir df2) -> ord1DirForest cmp df1 df2
(NodeFile _, NodeDir _) -> LT
(NodeDir _, NodeFile _) -> GT
newtype DirForest a = DirForest
{ unDirForest :: Map FilePath (DirTree a)
}
deriving stock (Show, Generic, Functor)
deriving (FromJSON, ToJSON) via (Autodocodec (DirForest a))
instance (Validity a) => Validity (DirForest a) where
validate df@(DirForest m) =
mconcat
[ genericValidate df,
decorateMap m $ \p dt ->
let isTopLevel p_ = parent p_ == [reldir|./|]
in case dt of
NodeFile _ ->
let rf = Path p :: Path Rel File
in mconcat
[ declare "There are no separators on this level" $ isTopLevel rf,
validate (Path p :: Path Rel File)
]
NodeDir (DirForest _) ->
let rd = Path (FP.addTrailingPathSeparator p) :: Path Rel Dir
in mconcat
[ declare "the path has no trailing path separator" $
not $
FP.hasTrailingPathSeparator p,
declare "There are no separators on this level" $ isTopLevel rd, -- We need this for equality with the files.
validate rd
]
]
instance Eq a => Eq (DirForest a) where
(==) = eq1DirForest (==)
instance Ord a => Ord (DirForest a) where
compare = ord1DirForest compare
instance Eq1 DirForest where
liftEq = eq1DirForest
instance Ord1 DirForest where
liftCompare = ord1DirForest
instance NFData a => NFData (DirForest a)
instance Foldable DirForest where
foldMap func (DirForest dtm) = foldMap (foldMap func) dtm
instance Traversable DirForest where
traverse func (DirForest dtm) = DirForest <$> traverse (traverse func) dtm
instance HasCodec a => HasCodec (DirForest a) where
codec =
named "DirForest" $
dimapCodec
DirForest
unDirForest
(codec :: JSONCodec (Map FilePath (DirTree a)))
eq1DirForest :: (a -> b -> Bool) -> DirForest a -> DirForest b -> Bool
eq1DirForest eq (DirForest m1) (DirForest m2) =
let l1 = M.toAscList m1
l2 = M.toAscList m2
in length l1 == length l2 && liftEq (\(p1, a1) (p2, a2) -> p1 == p2 && eq1DirTree eq a1 a2) l1 l2
ord1DirForest :: (a -> b -> Ordering) -> DirForest a -> DirForest b -> Ordering
ord1DirForest cmp (DirForest m1) (DirForest m2) =
let l1 = M.toAscList m1
l2 = M.toAscList m2
in liftCompare (\(p1, a1) (p2, a2) -> compare p1 p2 <> ord1DirTree cmp a1 a2) l1 l2
-- | File or Dir
data FOD a
= F a
| D
deriving (Show, Eq, Ord, Generic, Functor)
instance Validity a => Validity (FOD a)
-- | The empty forest
empty :: DirForest a
empty = DirForest M.empty
-- | True iff the forest is entirely empty
null :: DirForest a -> Bool
null (DirForest dtm) = M.null dtm
-- | True iff there are only empty directories in the directory forest
nullFiles :: DirForest a -> Bool
nullFiles (DirForest df) = all goTree df
where
goTree = \case
NodeFile _ -> False
NodeDir df' -> nullFiles df'
singletonFile :: Path Rel File -> a -> DirForest a
singletonFile rp a =
case insertFile rp a empty of
Right df -> df
_ -> error "There can't have been anything in the way in an empty dir forest."
singletonDir :: Path Rel Dir -> DirForest a
singletonDir rp =
case insertDir rp empty of
Right df -> df
_ -> error "There can't have been anything in the way in an empty dir forest."
mapWithPath :: (Path Rel File -> a -> b) -> DirForest a -> DirForest b
mapWithPath func df = runIdentity $ traverseWithPath (\a b -> Identity $ func a b) df
traverseWithPath :: forall a b f. Applicative f => (Path Rel File -> a -> f b) -> DirForest a -> f (DirForest b)
traverseWithPath func = goF [reldir|./|]
where
goF :: Path Rel Dir -> DirForest a -> f (DirForest b)
goF cur (DirForest ts) = DirForest <$> M.traverseWithKey (goT cur) ts
goT :: Path Rel Dir -> FilePath -> DirTree a -> f (DirTree b)
goT cur fp = \case
NodeFile v ->
let rf = cur </> fromJust (parseRelFile fp)
in NodeFile <$> func rf v
NodeDir df ->
let rd = cur </> fromJust (parseRelDir fp)
in NodeDir <$> goF rd df
traverseWithPath_ :: forall a b f. Applicative f => (Path Rel File -> a -> f b) -> DirForest a -> f ()
traverseWithPath_ func = goF [reldir|./|]
where
goF :: Path Rel Dir -> DirForest a -> f ()
goF cur (DirForest ts) = void $ M.traverseWithKey (goT cur) ts
goT :: Path Rel Dir -> FilePath -> DirTree a -> f ()
goT cur fp = \case
NodeFile v ->
let rf = cur </> fromJust (parseRelFile fp)
in void $ func rf v
NodeDir df ->
let rd = cur </> fromJust (parseRelDir fp)
in goF rd df
-- | Remove all empty directories from a 'DirForest'
--
-- This will return 'Nothing' if the root was also empty.
pruneEmptyDirs :: DirForest a -> Maybe (DirForest a)
pruneEmptyDirs (DirForest m) =
let m' = M.mapMaybe goTree m
in if M.null m' then Nothing else Just (DirForest m')
where
goTree :: DirTree a -> Maybe (DirTree a)
goTree dt = case dt of
NodeFile _ -> Just dt
NodeDir df -> NodeDir <$> pruneEmptyDirs df
anyEmptyDir :: DirForest a -> Bool
anyEmptyDir (DirForest m) = M.null m || any goTree m
where
goTree :: DirTree a -> Bool
goTree = \case
NodeFile _ -> False
NodeDir df -> anyEmptyDir df
lookup ::
forall a.
Path Rel File ->
DirForest a ->
Maybe a
lookup rp df = go df (FP.splitDirectories $ fromRelFile rp)
where
go :: DirForest a -> [FilePath] -> Maybe a
go (DirForest ts) =
\case
[] -> Nothing
[f] -> do
dt <- M.lookup f ts
case dt of
NodeFile contents -> Just contents
_ -> Nothing
(d : ds) -> do
dt <- M.lookup d ts
case dt of
NodeDir dt_ -> go dt_ ds
_ -> Nothing
insertFOD ::
forall a.
FilePath ->
FOD a ->
DirForest a ->
Either (InsertionError a) (DirForest a)
insertFOD fp fod dirForest = go [reldir|./|] dirForest (FP.splitDirectories fp)
where
node = case fod of
F a -> NodeFile a
D -> NodeDir empty
go ::
Path Rel Dir ->
DirForest a ->
[FilePath] ->
Either (InsertionError a) (DirForest a)
go cur df@(DirForest ts) =
\case
[] -> Right df -- Should not happen, but just insert nothing if it does.
[f] ->
-- The last piece
case M.lookup f ts of
Nothing ->
pure $ DirForest $ M.insert f node ts
Just dt ->
case dt of
NodeFile contents -> do
let rf = cur </> fromJust (parseRelFile f)
Left (FileInTheWay rf contents)
NodeDir df' -> case fod of
F _ -> do
let rd = cur </> fromJust (parseRelDir f)
Left (DirInTheWay rd df')
D -> pure df -- If it's already there, nothing changes
-- Not the last piece, must be a dir
(d : ds) ->
-- Check if this piece is already in the forest
case M.lookup d ts of
-- If it isn't, then we need to make it and try again
Nothing -> do
let df' = DirForest $ M.insert d (NodeDir empty) ts
go cur df' (d : ds)
-- If it is, then we can recurse down there.
Just dt ->
case dt of
NodeFile contents -> do
let rf = cur </> fromJust (parseRelFile d)
Left (FileInTheWay rf contents)
NodeDir df' -> do
let newCur = cur </> fromJust (parseRelDir d)
df'' <- go newCur df' ds
pure $ DirForest $ M.insert d (NodeDir df'') ts
insertFile ::
forall a.
Path Rel File ->
a ->
DirForest a ->
Either (InsertionError a) (DirForest a)
insertFile rp a = insertFOD (fromRelFile rp) (F a)
insertDir ::
forall a.
Path Rel Dir ->
DirForest a ->
Either (InsertionError a) (DirForest a)
insertDir rp = insertFOD (FP.dropTrailingPathSeparator $ fromRelDir rp) D
fromFileList :: [(Path Rel File, a)] -> Either (InsertionError a) (DirForest a)
fromFileList = foldM (flip $ uncurry insertFile) empty
toFileList :: DirForest a -> [(Path Rel File, a)]
toFileList = M.toList . toFileMap
data InsertValidation e a = InsertionErrors (NonEmpty (InsertionError e)) | NoInsertionErrors a
deriving (Show, Eq, Generic, Functor)
instance (Validity e, Validity a) => Validity (InsertValidation e a)
instance Applicative (InsertValidation e) where
pure = NoInsertionErrors
InsertionErrors es1 <*> InsertionErrors es2 = InsertionErrors $ es1 <> es2
InsertionErrors es <*> NoInsertionErrors _ = InsertionErrors es
NoInsertionErrors _ <*> InsertionErrors es = InsertionErrors es
NoInsertionErrors f <*> NoInsertionErrors a = NoInsertionErrors $ f a
unpackInsertValidation :: InsertValidation e a -> Either (NonEmpty (InsertionError e)) a
unpackInsertValidation = \case
InsertionErrors es -> Left es
NoInsertionErrors r -> Right r
-- Left-biased
union :: DirForest a -> DirForest a -> InsertValidation a (DirForest a)
union = unionWith const
-- Left-biased
unionWith :: (a -> a -> a) -> DirForest a -> DirForest a -> InsertValidation a (DirForest a)
unionWith func = unionWithKey (\_ a b -> func a b)
-- Left-biased
unionWithKey :: forall a. (Path Rel File -> a -> a -> a) -> DirForest a -> DirForest a -> InsertValidation a (DirForest a)
unionWithKey func = goForest "" -- Because "" FP.</> "anything" = "anything"
where
goForest :: FilePath -> DirForest a -> DirForest a -> InsertValidation a (DirForest a)
goForest base (DirForest dtm1) (DirForest dtm2) =
DirForest
<$> sequenceA
( M.unionWithKey
( \p e1 e2 -> case (e1, e2) of
(NoInsertionErrors m1, NoInsertionErrors m2) -> goTree (base FP.</> p) m1 m2
_ -> error "Should not happen because we just M.map-ed only NoInsertionErrors, but it did"
)
(M.map NoInsertionErrors dtm1)
(M.map NoInsertionErrors dtm2)
)
goTree :: FilePath -> DirTree a -> DirTree a -> InsertValidation a (DirTree a)
goTree base dt1 dt2 = case (dt1, dt2) of
(NodeDir df1, NodeDir df2) -> NodeDir <$> goForest base df1 df2
(NodeFile a1, NodeFile a2) -> NoInsertionErrors $ NodeFile $ func (fromJust $ parseRelFile base) a1 a2
(NodeFile a, NodeDir _) -> InsertionErrors $ FileInTheWay (fromJust $ parseRelFile base) a :| []
(NodeDir df, NodeFile _) -> InsertionErrors $ DirInTheWay (fromJust $ parseRelDir base) df :| []
unions :: [DirForest a] -> Either (InsertionError a) (DirForest a)
unions = foldM (\df1 df2 -> left NE.head $ unpackInsertValidation $ union df1 df2) empty
intersection :: DirForest a -> DirForest b -> DirForest a
intersection = intersectionWith const
intersectionWith :: (a -> b -> c) -> DirForest a -> DirForest b -> DirForest c
intersectionWith func = intersectionWithKey (\_ a b -> func a b)
intersectionWithKey :: forall a b c. (Path Rel File -> a -> b -> c) -> DirForest a -> DirForest b -> DirForest c
intersectionWithKey func = goForest "" -- Because "" FP.</> "anything" = "anything"
where
goForest :: FilePath -> DirForest a -> DirForest b -> DirForest c
goForest base (DirForest dtm1) (DirForest dtm2) =
DirForest $ M.mapMaybe id $ M.intersectionWithKey (\p m1 m2 -> goTree (base FP.</> p) m1 m2) dtm1 dtm2
goTree :: FilePath -> DirTree a -> DirTree b -> Maybe (DirTree c)
goTree base dt1 dt2 = case (dt1, dt2) of
(NodeDir df1_, NodeDir df2_) -> Just $ NodeDir $ goForest base df1_ df2_
(NodeFile f1, NodeFile f2) -> Just $ NodeFile $ func (fromJust $ parseRelFile base) f1 f2 -- TODO is this what we want?
_ -> Nothing
intersections :: [DirForest a] -> DirForest a
intersections = foldl' intersection empty
filter :: (a -> Bool) -> DirForest a -> DirForest a
filter func = filterWithKey (const func)
filterWithKey :: forall a. (Path Rel File -> a -> Bool) -> DirForest a -> DirForest a
filterWithKey filePred = goForest "" -- Because "" FP.</> "anything" = "anything"
where
goForest :: FilePath -> DirForest a -> DirForest a
goForest base (DirForest df) =
DirForest $
M.mapMaybeWithKey
(\p dt -> goTree (base FP.</> p) dt)
df
goTree :: FilePath -> DirTree a -> Maybe (DirTree a) -- Nothing means it will be removed
goTree base dt = case dt of
NodeFile cts -> do
rf <- parseRelFile base
if filePred rf cts then Just dt else Nothing
NodeDir df -> Just $ NodeDir $ goForest base df
filterHidden :: forall a. DirForest a -> DirForest a
filterHidden = goForest
where
goPair :: FilePath -> DirTree a -> Maybe (DirTree a)
goPair fp dt = if hiddenHere fp then Nothing else Just $ goTree dt
goForest :: DirForest a -> DirForest a
goForest (DirForest m) =
DirForest $ M.mapMaybeWithKey goPair m
goTree :: DirTree a -> DirTree a
goTree dt = case dt of
NodeFile _ -> dt
NodeDir df -> NodeDir $ goForest df
difference :: DirForest a -> DirForest b -> DirForest a
difference = differenceWith $ \_ _ -> Nothing
differenceWith :: (a -> b -> Maybe a) -> DirForest a -> DirForest b -> DirForest a
differenceWith func = differenceWithKey $ const func
differenceWithKey :: forall a b. (Path Rel File -> a -> b -> Maybe a) -> DirForest a -> DirForest b -> DirForest a
differenceWithKey func = goForest "" -- Because "" </> "anything" = "anything"
where
goForest :: FilePath -> DirForest a -> DirForest b -> DirForest a
goForest base (DirForest df1_) (DirForest df2_) =
DirForest $ M.differenceWithKey (\p dt1 dt2 -> goTree (base FP.</> p) dt1 dt2) df1_ df2_
goTree :: FilePath -> DirTree a -> DirTree b -> Maybe (DirTree a)
goTree base dt1 dt2 = case (dt1, dt2) of
(NodeFile v1, NodeFile v2) -> NodeFile <$> func (fromJust $ parseRelFile base) v1 v2
(NodeFile v, NodeDir _) -> Just $ NodeFile v -- TODO not sure what the semantics are here
(NodeDir df, NodeFile _) -> Just $ NodeDir df -- TODO not sure what the semantics are here
(NodeDir df1_, NodeDir df2_) -> Just $ NodeDir $ goForest base df1_ df2_
data InsertionError a
= FileInTheWay (Path Rel File) a
| DirInTheWay (Path Rel Dir) (DirForest a)
deriving (Show, Eq, Ord, Generic)
instance (Validity a) => Validity (InsertionError a)
fromFileMap :: Map (Path Rel File) a -> Either (InsertionError a) (DirForest a)
fromFileMap = foldM (\df (rf, cts) -> insertFile rf cts df) empty . M.toList
toFileMap :: DirForest a -> Map (Path Rel File) a
toFileMap = M.foldlWithKey go M.empty . unDirForest
where
go :: Map (Path Rel File) a -> FilePath -> DirTree a -> Map (Path Rel File) a
go m path =
\case
NodeFile contents ->
let rf = fromJust (parseRelFile path) -- Cannot fail if the original dirforest is valid
in M.insert rf contents m
NodeDir df ->
let rd = fromJust (parseRelDir path) -- Cannot fail if the original dirforest is valid
in M.union m $ M.mapKeys (rd </>) (toFileMap df)
fromMap :: Map FilePath (FOD a) -> Either (InsertionError a) (DirForest a)
fromMap = foldM (\df (rf, fod) -> insertFOD rf fod df) empty . M.toList
toMap :: DirForest a -> Map FilePath (FOD a)
toMap = M.foldlWithKey go M.empty . unDirForest
where
go :: Map FilePath (FOD a) -> FilePath -> DirTree a -> Map FilePath (FOD a)
go m path =
\case
NodeFile contents ->
M.insert path (F contents) m
NodeDir df ->
M.insert path D $ M.union m $ M.mapKeys (path FP.</>) (toMap df)
read ::
forall a b m.
(MonadIO m) =>
Path b Dir ->
(Path b File -> m a) ->
m (DirForest a)
read = readFiltered (const True) (const True)
readNonHidden ::
forall a b m.
(MonadIO m) =>
Path b Dir ->
(Path b File -> m a) ->
m (DirForest a)
readNonHidden = readNonHiddenFiltered (const True) (const True)
readNonHiddenFiltered ::
forall a b m.
(MonadIO m) =>
(Path b File -> Bool) ->
(Path b Dir -> Bool) ->
Path b Dir ->
(Path b File -> m a) ->
m (DirForest a)
readNonHiddenFiltered filePred dirPred root = readFiltered (\f -> go f && filePred f) (\d -> go d && dirPred d) root
where
go af = case stripProperPrefix root af of
Nothing -> True -- Whatever
Just rf -> not $ hiddenRel rf
readFiltered ::
forall a b m.
(MonadIO m) =>
(Path b File -> Bool) ->
(Path b Dir -> Bool) ->
Path b Dir ->
(Path b File -> m a) ->
m (DirForest a)
readFiltered filePred dirPred root readFunc = do
e <- doesDirExist root
if e
then do
fs <- walkDirAccumRel (Just decendHandler) outputWriter root
case unions fs of
Left _ -> error "There can't have been any intra-dir collisions, but there were."
Right r -> pure r
else pure empty
where
decendHandler :: Path Rel Dir -> [Path Rel Dir] -> [Path Rel File] -> m (WalkAction Rel)
decendHandler subdir dirs _ = do
let toExclude = Prelude.filter (not . dirPred . ((root </> subdir) </>)) dirs
pure $ WalkExclude toExclude
outputWriter :: Path Rel Dir -> [Path Rel Dir] -> [Path Rel File] -> m [DirForest a]
outputWriter subdir dirs files = do
df1 <- foldM goDir empty dirs
(: []) <$> foldM goFile df1 files
where
goDir :: DirForest a -> Path Rel Dir -> m (DirForest a)
goDir df p =
let path = root </> subdir </> p
in if dirPred path
then case insertDir (subdir </> p) df of
Left _ ->
error
"There can't have been anything in the way while reading a dirforest, but there was."
Right df' -> pure df'
else pure df
goFile :: DirForest a -> Path Rel File -> m (DirForest a)
goFile df p =
let path = root </> subdir </> p
in if filePred path
then do
contents <- readFunc path
case insertFile (subdir </> p) contents df of
Left _ ->
error
"There can't have been anything in the way while reading a dirforest, but there was."
Right df' -> pure df'
else pure df
readOneLevel ::
forall a b m.
(MonadIO m) =>
Path b Dir ->
(Path b File -> m a) ->
m (DirForest a)
readOneLevel = readOneLevelFiltered (const True) (const True)
readOneLevelNonHidden ::
forall a b m.
(MonadIO m) =>
Path b Dir ->
(Path b File -> m a) ->
m (DirForest a)
readOneLevelNonHidden = readOneLevelNonHiddenFiltered (const True) (const True)
readOneLevelNonHiddenFiltered ::
forall a b m.
(MonadIO m) =>
(Path b File -> Bool) ->
(Path b Dir -> Bool) ->
Path b Dir ->
(Path b File -> m a) ->
m (DirForest a)
readOneLevelNonHiddenFiltered filePred dirPred root = readOneLevelFiltered (\f -> go f && filePred f) (\d -> go d && dirPred d) root
where
go af = case stripProperPrefix root af of
Nothing -> True -- Whatever
Just rf -> not $ hiddenRel rf
readOneLevelFiltered ::
forall a b m.
(MonadIO m) =>
(Path b File -> Bool) ->
(Path b Dir -> Bool) ->
Path b Dir ->
(Path b File -> m a) ->
m (DirForest a)
readOneLevelFiltered filePred dirPred root readFunc = do
(dirs, files) <- fmap (fromMaybe ([], [])) $ liftIO $ forgivingAbsence $ listDirRel root
df1 <- foldM goDir empty dirs
foldM goFile df1 files
where
goDir :: DirForest a -> Path Rel Dir -> m (DirForest a)
goDir df p =
let path = root </> p
in if dirPred path
then case insertDir p df of
Left _ ->
error
"There can't have been anything in the way while reading a dirforest, but there was."
Right df' -> pure df'
else pure df
goFile :: DirForest a -> Path Rel File -> m (DirForest a)
goFile df p =
let path = root </> p
in if filePred path
then do
contents <- readFunc path
case insertFile p contents df of
Left _ ->
error
"There can't have been anything in the way while reading a dirforest, but there was."
Right df' -> pure df'
else pure df
write ::
forall a b.
(Show a, Ord a) =>
Path b Dir ->
DirForest a ->
(Path b File -> a -> IO ()) ->
IO ()
write root dirForest writeFunc = do
ensureDir root
forM_ (M.toList $ unDirForest dirForest) $ \(path, dt) ->
case dt of
NodeFile contents -> do
f <- parseRelFile path
let af = root </> f
writeFunc af contents
NodeDir df' -> do
d <- parseRelDir path
let ad = root </> d
write ad df' writeFunc
hiddenRel :: Path Rel t -> Bool
hiddenRel = any hiddenHere . FP.splitDirectories . toFilePath
hiddenHere :: FilePath -> Bool
hiddenHere [] = False -- Technically not possible, but fine
hiddenHere ('.' : _) = True
hiddenHere _ = False