{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE ViewPatterns #-} {-| 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 ( -- * Type Aliases CreateDirectoryFn , DoesFileExistFn , FindFilesFn , FindFilesByExtsFn , FindFilesByTypesFn , GetCurrentDirectoryFn , ListFilesFn , LoadFileFn -- * Polymorphic Record , FileSystem(..) , mkFileSystem -- * Traversing the File System , findFiles , findFilesByExts , findFilesByTypes , listFiles , loadFile -- * Working with Files Metadata , fileExtension -- * Other , excludePaths ) where import Headroom.Configuration.Types ( CtHeadersConfig ) import Headroom.Data.Regex ( Regex , match ) import Headroom.FileType ( listExtensions ) import Headroom.FileType.Types ( FileType ) 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 -------------------------------- TYPE ALIASES -------------------------------- -- | Type of a function that creates new empty directory on the given path. type CreateDirectoryFn m = FilePath -- ^ path of new directory -> m () -- ^ /IO/ action result -- | Type of a function that returns 'True' if the argument file exists and is -- not a directory, and 'False' otherwise. type DoesFileExistFn m = FilePath -- ^ path to check -> m Bool -- ^ whether the given path is existing file -- | Type of a function that recursively finds files on given path whose -- filename matches the predicate. type FindFilesFn m = FilePath -- ^ path to search -> (FilePath -> Bool) -- ^ predicate to match filename -> m [FilePath] -- ^ found files -- | Type of a function that recursively finds files on given path by file -- extensions. type FindFilesByExtsFn m = FilePath -- ^ path to search -> [Text] -- ^ list of file extensions (without dot) -> m [FilePath] -- ^ list of found files -- | Type of a function that recursively find files on given path by their -- file types. type FindFilesByTypesFn m = CtHeadersConfig -- ^ configuration of license headers -> [FileType] -- ^ list of file types -> FilePath -- ^ path to search -> m [FilePath] -- ^ list of found files -- | Type of a function that obtains the current working directory as an -- absolute path. type GetCurrentDirectoryFn m = m FilePath -- | Type of a function that recursively find all files on given path. If file -- reference is passed instead of directory, such file path is returned. type ListFilesFn m = FilePath -- ^ path to search -> m [FilePath] -- ^ list of found files -- | Type of a function that loads file content in UTF8 encoding. type LoadFileFn m = FilePath -- ^ file path -> m Text -- ^ file content ----------------------------- POLYMORPHIC RECORD ----------------------------- -- | /Polymorphic record/ composed of file system /IO/ function types, allowing -- to abstract over concrete implementation. Whenever you need to use effectful -- functions from this module, consider using this record instead of using them -- directly, as it allows you to use different records for production code and -- for testing, which is not as easy if you wire some of the provided functions -- directly. data FileSystem m = FileSystem { fsCreateDirectory :: CreateDirectoryFn m -- ^ Function that creates new empty directory on the given path. , fsDoesFileExist :: DoesFileExistFn m -- ^ Function that returns 'True' if the argument file exists and is not -- a directory, and 'False' otherwise. , fsFindFiles :: FindFilesFn m -- ^ Function that recursively finds files on given path whose filename -- matches the predicate. , fsFindFilesByExts :: FindFilesByExtsFn m -- ^ Function that recursively finds files on given path by file extensions. , fsFindFilesByTypes :: FindFilesByTypesFn m -- ^ Function that recursively find files on given path by their file types. , fsGetCurrentDirectory :: GetCurrentDirectoryFn m -- ^ Function that obtains the current working directory as an absolute path. , fsListFiles :: ListFilesFn m -- ^ Function that recursively find all files on given path. If file reference -- is passed instead of directory, such file path is returned. , fsLoadFile :: LoadFileFn m -- ^ Function that loads file content in UTF8 encoding. } -- | Creates new 'FileSystem' that performs actual disk /IO/ operations. mkFileSystem :: MonadIO m => FileSystem m mkFileSystem = FileSystem { fsCreateDirectory = createDirectory , fsDoesFileExist = doesFileExist , fsFindFiles = findFiles , fsFindFilesByExts = findFilesByExts , fsFindFilesByTypes = findFilesByTypes , fsGetCurrentDirectory = getCurrentDirectory , fsListFiles = listFiles , fsLoadFile = loadFile } ------------------------------ PUBLIC FUNCTIONS ------------------------------ -- | Recursively finds files on given path whose filename matches the predicate. findFiles :: MonadIO m => FindFilesFn m findFiles path predicate = fmap (filter predicate) (listFiles path) -- | Recursively finds files on given path by file extensions. findFilesByExts :: MonadIO m => FindFilesByExtsFn m findFilesByExts path exts = findFiles path predicate where predicate p = any (`isExtensionOf` p) (fmap T.unpack exts) -- | Recursively find files on given path by their file types. findFilesByTypes :: MonadIO m => FindFilesByTypesFn m findFilesByTypes headersConfig types path = findFilesByExts path (types >>= listExtensions headersConfig) -- | Recursively find all files on given path. If file reference is passed -- instead of directory, such file path is returned. listFiles :: MonadIO m => ListFilesFn m listFiles fileOrDir = do isDir <- doesDirectoryExist fileOrDir if isDir then listDirectory fileOrDir else pure [fileOrDir] where listDirectory dir = do names <- getDirectoryContents dir let filteredNames = filter (`notElem` [".", ".."]) names paths <- forM filteredNames $ \name -> do let path = dir </> name isDirectory <- doesDirectoryExist path if isDirectory then listFiles path else pure [path] pure $ concat paths -- | Returns file extension for given path (if file), or nothing otherwise. -- -- >>> fileExtension "path/to/some/file.txt" -- Just "txt" fileExtension :: FilePath -- ^ path from which to extract file extension -> Maybe Text -- ^ extracted file extension fileExtension (takeExtension -> '.' : xs) = Just $ T.pack xs fileExtension _ = Nothing -- | Loads file content in UTF8 encoding. loadFile :: MonadIO m => LoadFileFn m loadFile = readFileUtf8 -- | Takes list of patterns and file paths and returns list of file paths where -- those matching the given patterns are excluded. -- -- >>> import Headroom.Data.Regex (re) -- >>> :set -XQuasiQuotes -- >>> excludePaths [[re|\.hidden|], [re|zzz|]] ["foo/.hidden", "test/bar", "x/zzz/e"] -- ["test/bar"] excludePaths :: [Regex] -- ^ patterns describing paths to exclude -> [FilePath] -- ^ list of file paths -> [FilePath] -- ^ resulting list of file paths excludePaths _ [] = [] excludePaths [] paths = paths excludePaths patterns paths = L.filter excluded paths where excluded item = all (\p -> isNothing $ match p (T.pack item)) patterns