packages feed

talash-0.1.0.0: src/Talash/Files.hs

-- | A potentially too simple interface for getting candidates for search from file trees. Doesn't follow symbolic links. For a better solution to this use
-- [unix-recursive](https://hackage.haskell.org/package/unix-recursive).
module Talash.Files (-- * Types
                     Conf (..) , FindConf (..) , FindInDirs (..) , FileTree (..)
                     -- * File Collection
                    , defConf , withExts , ignoreExts , findWithExts , findFilesInDirs , executables
                     -- * Internal Details
                    , dirContentsWith , fileTreeWith , minify , flatten , ext) where

import Control.Exception
import qualified Data.ByteString.Char8 as B
import qualified Data.HashSet as S
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import Intro
import System.Posix.Directory.ByteString
import System.Posix.Env.ByteString
import System.Posix.Files.ByteString

-- Configruation for the search when recursivley constructing the file tree.
data Conf = Conf {
              -- | Test for whether to include a file in the file tree. The second argument is the base name of the file.
              includeFile :: FileStatus -> ByteString -> IO Bool ,
              -- | Test used to determine whether to enter a directory to search for files.
              filterPath :: ByteString -> Bool }

-- | A simple type to represent a search either for a specific set of extensions or esle for excluding a specific set of extensions. An extension here
-- is just the part of the filename after the last '.' i.e this module doesn't handle multiple extensions.
data FindConf = Find !(S.HashSet ByteString) | Ignore !(S.HashSet ByteString) deriving Show


data FindInDirs = FindInDirs {
                    -- | The configuration of finding or excluding the extensions for this set of directories.
                    confLocal :: FindConf ,
                    -- | The list of directories to which this configuration should apply.
                    dirsLocal :: [ByteString]}

data FileTree a = Dir !a -- ^ The root directory
                      !(V.Vector a) -- ^ The files in the root directory that are not subdirectories
                      !(V.Vector (FileTree a)) -- ^ The vector of trees formed by subdirectories
                        deriving (Eq , Show)

-- | Default configuration, include every file and search directory.
defConf :: Conf
defConf = Conf (const . const $ pure True) (const True)

-- | Given the configuration and a directory returns a vector where the Left elements are the files in the directory that pass the `includeFile` test while
--   the Right elements are subdirectories that pass the `filterPath` test.
{-# INLINEABLE dirContentsWith #-}
dirContentsWith :: Conf -> ByteString -> IO (V.Vector (Either ByteString ByteString))
dirContentsWith c d = bracket (openDirStream d) closeDirStream (V.unfoldrM (\s -> map (\d -> if d == Left "" then Nothing else Just (d , s)) . go $ s))
  where
    go s = nm =<< readDirStream s
      where
        nm f
          | f == ""                                         = pure (Left "")
          | otherwise                                       = hr =<< getSymbolicLinkStatus f
          where
            hr fs
              | isDirectory fs                              = hd
              | otherwise                                   = ifM (includeFile c fs f) (pure . Left $ f) (go s)
            hd
              | filterPath c f && f /= "." && f /= ".."     = ifM (fileAccess f True False False) (pure . Right $ f) (go s)
              | otherwise                                   = go s

-- | Constructs the file tree with the given the second argument at the root according to the given configuration.
{-# INLINEABLE fileTreeWith #-}
fileTreeWith :: Conf -> ByteString -> IO (FileTree Text)
fileTreeWith c d = bracket getWorkingDirectory changeWorkingDirectory (const $ changeWorkingDirectory d *> (go =<< dirContentsWith c "."))
  where
    go v = (\(a , b) -> Dir (T.decodeUtf8 d) a <$!> V.mapM (fileTreeWith c) b) . V.partitionWith (first T.decodeUtf8) $ v

-- | Collapses the directories with only subdirectory and no other files.
{-# INLINEABLE minify #-}
minify :: FileTree Text -> FileTree Text
minify (Dir d f t)
  | f == V.empty && V.length t == 1  = (\(Dir d' f' t') -> Dir (d <> d') f' t') (V.unsafeHead t)
  | otherwise                        = Dir d f . V.map minify $ t

-- | Flattens the fileTree by completing the paths of the file relative to that of root directory.
{-# INLINEABLE flatten #-}
flatten :: FileTree Text -> V.Vector Text
flatten (Dir d f t) = V.concatMap go t <> V.map ((d <> "/") <>) f
  where
    go (Dir d' !f' t') = flatten (Dir (d <> "/" <> d') f' t')

{-# INLINABLE withExts #-}
withExts :: [ByteString] -- ^ The set of extensions to search for
  -> FindConf
withExts = Find . S.fromList

{-# INLINABLE ignoreExts #-}
ignoreExts :: [ByteString] -- ^ The set of extensions to ignore.
  -> FindConf
ignoreExts = Ignore . S.fromList

-- | The last extension of a file. Returns empty bytestring if there is none.
{-# INLINABLE ext #-}
ext :: ByteString -> ByteString
ext c = if e == c then mempty else e
  where
    e = B.takeWhileEnd (/= '.') c

-- | Find files in the given set of directories that either have a specific extension (`Find` case) or else excluding a certain set of extensiosn (`Ignore` case).
{-# INLINE findWithExts #-}
findWithExts :: FindInDirs -> IO (V.Vector (FileTree Text))
findWithExts (FindInDirs c d) = V.mapM (fileTreeWith ch) . V.fromList $ d
  where
    ch
      | Find   es <- c     = defConf {includeFile = \ !s !n -> pure $ isRegularFile s && S.member (ext n) es}
      | Ignore es <- c     = defConf {includeFile = \ !s !n -> pure $ isRegularFile s && not (S.member (ext n) es)}


-- | Like `findWithExts` but applied to mutliple lists of directories each with their own configuration of extensions.
{-# INLINABLE findFilesInDirs #-}
findFilesInDirs :: [FindInDirs] -> IO (V.Vector (FileTree Text))
findFilesInDirs = foldr (\a t -> t <> findWithExts a) (pure mempty)
-- | Find all the executables in PATH
executables :: IO (V.Vector Text)
executables = foldr (\a t -> t <> map (V.map (T.takeWhileEnd (/= '/')) . flatten) (fileTreeWith cl a)) (pure V.empty) . B.split ':' =<< getEnvDefault "PATH" ""
  where
    cl = defConf { filterPath = const False , includeFile = \ s p ->  map ((isRegularFile s || isSymbolicLink s) &&) . fileAccess p False False $ True}