{-# LANGUAGE NoImplicitPrelude #-}

{-|
Module      : Headroom.FileSystem
Description : Operations related to files and file system
Copyright   : (c) 2019-2020 Vaclav Svejcar
License     : BSD-3-Clause
Maintainer  : vaclav.svejcar@gmail.com
Stability   : experimental
Portability : POSIX

Module providing functions for working with the local file system, its file and
directories.
-}

module Headroom.FileSystem
  ( -- * Traversing the File System
    findFiles
  , findFilesByExts
  , findFilesByTypes
  , listFiles
  , loadFile
    -- * Working with Files/Directories
  , doesFileExist
  , getCurrentDirectory
  , createDirectory
    -- * Working with Files Metadata
  , fileExtension
    -- * Other
  , excludePaths
  )
where

import           Headroom.FileType              ( listExtensions )
import           Headroom.Regex                 ( compile'
                                                , joinPatterns
                                                , match'
                                                )
import           Headroom.Types                 ( FileType
                                                , HeadersConfig(..)
                                                )
import           RIO
import           RIO.Directory                  ( createDirectory
                                                , doesDirectoryExist
                                                , doesFileExist
                                                , getCurrentDirectory
                                                , getDirectoryContents
                                                )
import           RIO.FilePath                   ( isExtensionOf
                                                , takeExtension
                                                , (</>)
                                                )
import qualified RIO.List                      as L
import qualified RIO.Text                      as T



-- | Recursively finds files on given path whose filename matches the predicate.
findFiles :: MonadIO m
          => FilePath           -- ^ path to search
          -> (FilePath -> Bool) -- ^ predicate to match filename
          -> m [FilePath]       -- ^ found files
findFiles :: FilePath -> (FilePath -> Bool) -> m [FilePath]
findFiles path :: FilePath
path predicate :: FilePath -> Bool
predicate = ([FilePath] -> [FilePath]) -> m [FilePath] -> m [FilePath]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap ((FilePath -> Bool) -> [FilePath] -> [FilePath]
forall a. (a -> Bool) -> [a] -> [a]
filter FilePath -> Bool
predicate) (FilePath -> m [FilePath]
forall (m :: * -> *). MonadIO m => FilePath -> m [FilePath]
listFiles FilePath
path)


-- | Recursively finds files on given path by file extensions.
findFilesByExts :: MonadIO m
                => FilePath     -- ^ path to search
                -> [Text]       -- ^ list of file extensions (without dot)
                -> m [FilePath] -- ^ list of found files
findFilesByExts :: FilePath -> [Text] -> m [FilePath]
findFilesByExts path :: FilePath
path exts :: [Text]
exts = FilePath -> (FilePath -> Bool) -> m [FilePath]
forall (m :: * -> *).
MonadIO m =>
FilePath -> (FilePath -> Bool) -> m [FilePath]
findFiles FilePath
path FilePath -> Bool
predicate
  where predicate :: FilePath -> Bool
predicate p :: FilePath
p = (FilePath -> Bool) -> [FilePath] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (FilePath -> FilePath -> Bool
`isExtensionOf` FilePath
p) ((Text -> FilePath) -> [Text] -> [FilePath]
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
fmap Text -> FilePath
T.unpack [Text]
exts)


-- | Recursively find files on given path by their file types.
findFilesByTypes :: MonadIO m
                 => HeadersConfig -- ^ configuration of license headers
                 -> [FileType]    -- ^ list of file types
                 -> FilePath      -- ^ path to search
                 -> m [FilePath]  -- ^ list of found files
findFilesByTypes :: HeadersConfig -> [FileType] -> FilePath -> m [FilePath]
findFilesByTypes headersConfig :: HeadersConfig
headersConfig types :: [FileType]
types path :: FilePath
path =
  FilePath -> [Text] -> m [FilePath]
forall (m :: * -> *).
MonadIO m =>
FilePath -> [Text] -> m [FilePath]
findFilesByExts FilePath
path ([FileType]
types [FileType] -> (FileType -> [Text]) -> [Text]
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= HeadersConfig -> FileType -> [Text]
listExtensions HeadersConfig
headersConfig)


-- | Recursively find all files on given path. If file reference is passed
-- instead of directory, such file path is returned.
listFiles :: MonadIO m
          => FilePath     -- ^ path to search
          -> m [FilePath] -- ^ list of found files
listFiles :: FilePath -> m [FilePath]
listFiles fileOrDir :: FilePath
fileOrDir = do
  Bool
isDir <- FilePath -> m Bool
forall (m :: * -> *). MonadIO m => FilePath -> m Bool
doesDirectoryExist FilePath
fileOrDir
  if Bool
isDir then FilePath -> m [FilePath]
forall (m :: * -> *). MonadIO m => FilePath -> m [FilePath]
listDirectory FilePath
fileOrDir else [FilePath] -> m [FilePath]
forall (f :: * -> *) a. Applicative f => a -> f a
pure [FilePath
fileOrDir]
 where
  listDirectory :: FilePath -> m [FilePath]
listDirectory dir :: FilePath
dir = do
    [FilePath]
names <- FilePath -> m [FilePath]
forall (m :: * -> *). MonadIO m => FilePath -> m [FilePath]
getDirectoryContents FilePath
dir
    let filteredNames :: [FilePath]
filteredNames = (FilePath -> Bool) -> [FilePath] -> [FilePath]
forall a. (a -> Bool) -> [a] -> [a]
filter (FilePath -> [FilePath] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`notElem` [".", ".."]) [FilePath]
names
    [[FilePath]]
paths <- [FilePath] -> (FilePath -> m [FilePath]) -> m [[FilePath]]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
t a -> (a -> m b) -> m (t b)
forM [FilePath]
filteredNames ((FilePath -> m [FilePath]) -> m [[FilePath]])
-> (FilePath -> m [FilePath]) -> m [[FilePath]]
forall a b. (a -> b) -> a -> b
$ \name :: FilePath
name -> do
      let path :: FilePath
path = FilePath
dir FilePath -> FilePath -> FilePath
</> FilePath
name
      Bool
isDirectory <- FilePath -> m Bool
forall (m :: * -> *). MonadIO m => FilePath -> m Bool
doesDirectoryExist FilePath
path
      if Bool
isDirectory then FilePath -> m [FilePath]
forall (m :: * -> *). MonadIO m => FilePath -> m [FilePath]
listFiles FilePath
path else [FilePath] -> m [FilePath]
forall (f :: * -> *) a. Applicative f => a -> f a
pure [FilePath
path]
    [FilePath] -> m [FilePath]
forall (f :: * -> *) a. Applicative f => a -> f a
pure ([FilePath] -> m [FilePath]) -> [FilePath] -> m [FilePath]
forall a b. (a -> b) -> a -> b
$ [[FilePath]] -> [FilePath]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [[FilePath]]
paths


-- | Returns file extension for given path (if file), or nothing otherwise.
--
-- >>> fileExtension "path/to/some/file.txt"
-- Just "txt"
fileExtension :: FilePath -> Maybe Text
fileExtension :: FilePath -> Maybe Text
fileExtension path :: FilePath
path = case FilePath -> FilePath
takeExtension FilePath
path of
  '.' : xs :: FilePath
xs -> Text -> Maybe Text
forall a. a -> Maybe a
Just (Text -> Maybe Text) -> Text -> Maybe Text
forall a b. (a -> b) -> a -> b
$ FilePath -> Text
T.pack FilePath
xs
  _        -> Maybe Text
forall a. Maybe a
Nothing


-- | Loads file content in UTF8 encoding.
loadFile :: MonadIO m
         => FilePath -- ^ file path
         -> m Text   -- ^ file content
loadFile :: FilePath -> m Text
loadFile = FilePath -> m Text
forall (m :: * -> *). MonadIO m => FilePath -> m Text
readFileUtf8


-- | Takes list of patterns and file paths and returns list of file paths where
-- those matching the given patterns are excluded.
--
-- >>> excludePaths ["\\.hidden", "zzz"] ["foo/.hidden", "test/bar", "x/zzz/e"]
-- ["test/bar"]
excludePaths :: [Text]     -- ^ patterns describing paths to exclude
             -> [FilePath] -- ^ list of file paths
             -> [FilePath] -- ^ resulting list of file paths
excludePaths :: [Text] -> [FilePath] -> [FilePath]
excludePaths _        []    = []
excludePaths []       paths :: [FilePath]
paths = [FilePath]
paths
excludePaths patterns :: [Text]
patterns paths :: [FilePath]
paths = Maybe Regex -> [FilePath]
go (Maybe Regex -> [FilePath]) -> Maybe Regex -> [FilePath]
forall a b. (a -> b) -> a -> b
$ Text -> Regex
compile' (Text -> Regex) -> Maybe Text -> Maybe Regex
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> [Text] -> Maybe Text
joinPatterns [Text]
patterns
 where
  go :: Maybe Regex -> [FilePath]
go Nothing      = [FilePath]
paths
  go (Just regex :: Regex
regex) = (FilePath -> Bool) -> [FilePath] -> [FilePath]
forall a. (a -> Bool) -> [a] -> [a]
L.filter (Maybe [Text] -> Bool
forall a. Maybe a -> Bool
isNothing (Maybe [Text] -> Bool)
-> (FilePath -> Maybe [Text]) -> FilePath -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Regex -> Text -> Maybe [Text]
match' Regex
regex (Text -> Maybe [Text])
-> (FilePath -> Text) -> FilePath -> Maybe [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. FilePath -> Text
T.pack) [FilePath]
paths