path-io 1.3.3 → 1.8.2
raw patch · 7 files changed
Files
- CHANGELOG.md +79/−0
- LICENSE.md +1/−1
- Path/IO.hs +1874/−1398
- README.md +13/−8
- Setup.hs +0/−6
- path-io.cabal +67/−55
- tests/Main.hs +193/−137
CHANGELOG.md view
@@ -1,3 +1,82 @@+*Path IO follows [SemVer](https://semver.org/).*++## Path IO 1.8.2++* Maintenance release, compatibility with GHC 9.6, 9.8, and 9.10.++## Path IO 1.8.1++* Fixed a “permission denied” error occurring when `copyDirRecur` is used to+ copy a tree with non-empty read-only directories. [PR+ 82](https://github.com/mrkkrp/path-io/pull/82).++## Path IO 1.8.0++* Added instances of `AnyPath` for `SomeBase`. [PR+ 72](https://github.com/mrkkrp/path-io/pull/72).++## Path IO 1.7.0++* Added `doesPathExist`, `getFileSize`, `renamePath`, and+ `removePathForcibly`.++## Path IO 1.6.3++* Fixed a bug that caused `removeDirLink` fail on Linux because of a+ trailing slash that used to be passed to the underlying+ `removeDirectoryLink` function from the `directory` package. [Issue+ 59](https://github.com/mrkkrp/path-io/issues/59).++* Works with GHC 9.0.1.++## Path IO 1.6.2++* Fixed a bug in the `findFilesWith` and based on it `findFiles` functions.++## Path IO 1.6.1++* Fixed a space leak in `walkDirAccum`. [Issue+ 55](https://github.com/mrkkrp/path-io/issues/55).++## Path IO 1.6.0++* Changed how `copyDirRecur` and `copyDirRecur'` functions work. Previously,+ the functions created empty directories in the destination directory when+ the source directory contained directory symlinks. The symlinked+ directories were not recursively traversed. It also copied symlinked files+ creating normal regular files in the target directory as the result. This+ is fixed so that the function now behaves much like the `cp` utility, not+ traversing symlinked directories, but recreating symlinks in the target+ directory according to their targets in the source directory.++* Fixed a bug in `createDirLink` which would always fail complaining that+ its destination location does not exist.++* Dropped support for GHC 8.2.++## Path IO 1.5.0++* Dropped support for GHC 8.0 and older.+* Added new functions: `getXdgDirList`, `createFileLink`, `createDirLink`,+ `removeDirLink`, `getSymlinkTarget`.++## Path IO 1.4.2++* Fixed various bugs in `listDirRecurRel`, `walkDirRel`, and+ `walkDirAccumRel` and clarified their behavior in the docs.++## Path IO 1.4.1++* Fixed a bug in `walkDirRel` that resulted in `NotAProperPrefix` exception+ every time the function was called.++## Path IO 1.4.0++* Added relative versions of some actions: `listDirRel`, `listDirRecurRel`,+ `walkDirRel`, and `walkDirAccumRel`.++* Dropped support for GHC 7.8.+ ## Path IO 1.3.3 * (Hopefully) fixed test suite failure with Cabal 2.0 and GHC 8.2.1.
LICENSE.md view
@@ -1,4 +1,4 @@-Copyright © 2016–2017 Mark Karpov+Copyright © 2016–present Mark Karpov All rights reserved.
Path/IO.hs view
@@ -1,1399 +1,1875 @@--- |--- Module : Path.IO--- Copyright : © 2016–2017 Mark Karpov--- License : BSD 3 clause------ Maintainer : Mark Karpov <markkarpov92@gmail.com>--- Stability : experimental--- Portability : portable------ This module provides an interface to "System.Directory" for users of the--- "Path" module. It also implements commonly used primitives like recursive--- scanning and copying of directories, working with temporary--- files\/directories, etc.--{-# LANGUAGE CPP #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE TypeFamilies #-}--module Path.IO- ( -- * Actions on directories- createDir- , createDirIfMissing- , ensureDir- , removeDir- , removeDirRecur- , renameDir- , listDir- , listDirRecur- , copyDirRecur- , copyDirRecur'- -- ** Walking directory trees- , WalkAction (..)- , walkDir- , walkDirAccum- -- ** Current working directory- , getCurrentDir- , setCurrentDir- , withCurrentDir- -- * Pre-defined directories- , getHomeDir- , getAppUserDataDir- , getUserDocsDir- , getTempDir-#if MIN_VERSION_directory(1,2,3)- , XdgDirectory (..)- , getXdgDir-#endif- -- * Path transformation- , AnyPath (..)- , resolveFile- , resolveFile'- , resolveDir- , resolveDir'- -- * Actions on files- , removeFile- , renameFile- , copyFile- , findExecutable- , findFile- , findFiles- , findFilesWith- -- * Symbolic links- , isSymlink- -- * Temporary files and directories- , withTempFile- , withTempDir- , withSystemTempFile- , withSystemTempDir- , openTempFile- , openBinaryTempFile- , createTempDir- -- * Existence tests- , doesFileExist- , doesDirExist- , isLocationOccupied- , forgivingAbsence- , ignoringAbsence- -- * Permissions- , D.Permissions- , D.emptyPermissions- , D.readable- , D.writable- , D.executable- , D.searchable- , D.setOwnerReadable- , D.setOwnerWritable- , D.setOwnerExecutable- , D.setOwnerSearchable- , getPermissions- , setPermissions- , copyPermissions- -- * Timestamps-#if MIN_VERSION_directory(1,2,3)- , getAccessTime- , setAccessTime- , setModificationTime-#endif- , getModificationTime )-where--import Control.Arrow ((***))-import Control.Monad-import Control.Monad.Catch-import Control.Monad.IO.Class (MonadIO (..))-import Control.Monad.Trans.Class (lift)-import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)-import Control.Monad.Trans.Writer.Lazy (execWriterT, tell)-import Data.Either (lefts, rights)-import Data.List ((\\))-import Data.Time (UTCTime)-import Path-import System.IO (Handle)-import System.IO.Error (isDoesNotExistError)-import qualified Data.DList as DList-import qualified Data.Set as S-import qualified System.Directory as D-import qualified System.FilePath as F-import qualified System.IO.Temp as T-import qualified System.PosixCompat.Files as P--#if MIN_VERSION_directory(1,2,3)-import System.Directory (XdgDirectory)-#endif-#if !MIN_VERSION_base(4,8,0)-import Data.Monoid (Monoid)-#endif--------------------------------------------------------------------------------- Actions on directories---- | @'createDir' dir@ creates a new directory @dir@ which is initially--- empty, or as near to empty as the operating system allows.------ The operation may fail with:------ * 'isPermissionError' \/ 'PermissionDenied'--- The process has insufficient privileges to perform the operation.--- @[EROFS, EACCES]@------ * 'isAlreadyExistsError' \/ 'AlreadyExists'--- The operand refers to a directory that already exists.--- @ [EEXIST]@------ * 'HardwareFault'--- A physical I\/O error has occurred.--- @[EIO]@------ * 'InvalidArgument'--- The operand is not a valid directory name.--- @[ENAMETOOLONG, ELOOP]@------ * 'NoSuchThing'--- There is no path to the directory.--- @[ENOENT, ENOTDIR]@------ * 'ResourceExhausted' Insufficient resources (virtual memory, process--- file descriptors, physical disk space, etc.) are available to perform the--- operation. @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@------ * 'InappropriateType'--- The path refers to an existing non-directory object.--- @[EEXIST]@--createDir :: MonadIO m => Path b Dir -> m ()-createDir = liftD D.createDirectory---- | @'createDirIfMissing' parents dir@ creates a new directory @dir@ if it--- doesn't exist. If the first argument is 'True' the function will also--- create all parent directories if they are missing.--createDirIfMissing :: MonadIO m- => Bool -- ^ Create its parents too?- -> Path b Dir -- ^ The path to the directory you want to make- -> m ()-createDirIfMissing p = liftD (D.createDirectoryIfMissing p)---- | Ensure that a directory exists creating it and its parent directories--- if necessary. This is just a handy shortcut:------ > ensureDir = createDirIfMissing True------ @since 0.3.1--ensureDir :: MonadIO m => Path b Dir -> m ()-ensureDir = createDirIfMissing True---- | @'removeDir' dir@ removes an existing directory @dir@. The--- implementation may specify additional constraints which must be satisfied--- before a directory can be removed (e.g. the directory has to be empty, or--- may not be in use by other processes). It is not legal for an--- implementation to partially remove a directory unless the entire--- directory is removed. A conformant implementation need not support--- directory removal in all situations (e.g. removal of the root directory).------ The operation may fail with:------ * 'HardwareFault'--- A physical I\/O error has occurred.--- @[EIO]@------ * 'InvalidArgument'--- The operand is not a valid directory name.--- @[ENAMETOOLONG, ELOOP]@------ * 'isDoesNotExistError' \/ 'NoSuchThing'--- The directory does not exist.--- @[ENOENT, ENOTDIR]@------ * 'isPermissionError' \/ 'PermissionDenied'--- The process has insufficient privileges to perform the operation.--- @[EROFS, EACCES, EPERM]@------ * 'UnsatisfiedConstraints'--- Implementation-dependent constraints are not satisfied.--- @[EBUSY, ENOTEMPTY, EEXIST]@------ * 'UnsupportedOperation'--- The implementation does not support removal in this situation.--- @[EINVAL]@------ * 'InappropriateType'--- The operand refers to an existing non-directory object.--- @[ENOTDIR]@--removeDir :: MonadIO m => Path b Dir -> m ()-removeDir = liftD D.removeDirectory---- | @'removeDirRecur' dir@ removes an existing directory @dir@ together--- with its contents and sub-directories. Within this directory, symbolic--- links are removed without affecting their targets.--removeDirRecur :: MonadIO m => Path b Dir -> m ()-removeDirRecur = liftD D.removeDirectoryRecursive---- |@'renameDir' old new@ changes the name of an existing directory from--- @old@ to @new@. If the @new@ directory already exists, it is atomically--- replaced by the @old@ directory. If the @new@ directory is neither the--- @old@ directory nor an alias of the @old@ directory, it is removed as if--- by 'removeDir'. A conformant implementation need not support renaming--- directories in all situations (e.g. renaming to an existing directory, or--- across different physical devices), but the constraints must be--- documented.------ On Win32 platforms, @renameDir@ fails if the @new@ directory already--- exists.------ The operation may fail with:------ * 'HardwareFault'--- A physical I\/O error has occurred.--- @[EIO]@------ * 'InvalidArgument'--- Either operand is not a valid directory name.--- @[ENAMETOOLONG, ELOOP]@------ * 'isDoesNotExistError' \/ 'NoSuchThing'--- The original directory does not exist, or there is no path to the target.--- @[ENOENT, ENOTDIR]@------ * 'isPermissionError' \/ 'PermissionDenied'--- The process has insufficient privileges to perform the operation.--- @[EROFS, EACCES, EPERM]@------ * 'ResourceExhausted'--- Insufficient resources are available to perform the operation.--- @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@------ * 'UnsatisfiedConstraints'--- Implementation-dependent constraints are not satisfied.--- @[EBUSY, ENOTEMPTY, EEXIST]@------ * 'UnsupportedOperation'--- The implementation does not support renaming in this situation.--- @[EINVAL, EXDEV]@------ * 'InappropriateType'--- Either path refers to an existing non-directory object.--- @[ENOTDIR, EISDIR]@--renameDir :: MonadIO m- => Path b0 Dir -- ^ Old name- -> Path b1 Dir -- ^ New name- -> m ()-renameDir = liftD2 D.renameDirectory---- | @'listDir' dir@ returns a list of /all/ entries in @dir@ without the--- special entries (@.@ and @..@). Entries are not sorted.------ The operation may fail with:------ * 'HardwareFault'--- A physical I\/O error has occurred.--- @[EIO]@------ * 'InvalidArgument'--- The operand is not a valid directory name.--- @[ENAMETOOLONG, ELOOP]@------ * 'isDoesNotExistError' \/ 'NoSuchThing'--- The directory does not exist.--- @[ENOENT, ENOTDIR]@------ * 'isPermissionError' \/ 'PermissionDenied'--- The process has insufficient privileges to perform the operation.--- @[EACCES]@------ * 'ResourceExhausted'--- Insufficient resources are available to perform the operation.--- @[EMFILE, ENFILE]@------ * 'InappropriateType'--- The path refers to an existing non-directory object.--- @[ENOTDIR]@--listDir :: MonadIO m- => Path b Dir -- ^ Directory to list- -> m ([Path Abs Dir], [Path Abs File]) -- ^ Sub-directories and files-listDir path = liftIO $ do- bpath <- makeAbsolute path- raw <- liftD D.getDirectoryContents bpath- items <- forM (raw \\ [".", ".."]) $ \item -> do- let ipath = toFilePath bpath F.</> item- isDir <- liftIO (D.doesDirectoryExist ipath)- if isDir- then Left `liftM` parseAbsDir ipath- else Right `liftM` parseAbsFile ipath- return (lefts items, rights items)---- | Similar to 'listDir', but recursively traverses every sub-directory--- /excluding symbolic links/, and returns all files and directories found.--- This can fail with the same exceptions as 'listDir'.------ __Note__: before version /1.3.0/, this function followed symlinks.--listDirRecur :: MonadIO m- => Path b Dir -- ^ Directory to list- -> m ([Path Abs Dir], [Path Abs File]) -- ^ Sub-directories and files-listDirRecur dir = (DList.toList *** DList.toList)- `liftM` walkDirAccum (Just excludeSymlinks) writer dir- where- excludeSymlinks _ subdirs _ =- WalkExclude `liftM` filterM isSymlink subdirs- writer _ ds fs = return (DList.fromList ds, DList.fromList fs)---- | Copies a directory recursively. It /does not/ follow symbolic links and--- preserves permissions when possible. If the destination directory already--- exists, new files and sub-directories complement its structure, possibly--- overwriting old files if they happen to have the same name as the new--- ones.------ __Note__: before version /1.3.0/, this function followed symlinks.--copyDirRecur :: (MonadIO m, MonadCatch m)- => Path b0 Dir -- ^ Source- -> Path b1 Dir -- ^ Destination- -> m ()-copyDirRecur = copyDirRecurGen True---- | The same as 'copyDirRecur', but it /does not/ preserve directory--- permissions. This may be useful, for example, if the directory you want--- to copy is “read-only”, but you want your copy to be editable.------ @since 1.1.0------ __Note__: before version /1.3.0/, this function followed symlinks.--copyDirRecur' :: (MonadIO m, MonadCatch m)- => Path b0 Dir -- ^ Source- -> Path b1 Dir -- ^ Destination- -> m ()-copyDirRecur' = copyDirRecurGen False---- | Generic version of 'copyDirRecur'. The first argument controls whether--- to preserve directory permissions or not. /Does not/ follow symbolic--- links.------ __Note__: before version /1.3.0/, this function followed symlinks.--copyDirRecurGen :: MonadIO m- => Bool -- ^ Should we preserve directory permissions?- -> Path b0 Dir -- ^ Source- -> Path b1 Dir -- ^ Destination- -> m ()-copyDirRecurGen p src dest = liftIO $ do- bsrc <- makeAbsolute src- bdest <- makeAbsolute dest- (dirs, files) <- listDirRecur bsrc- let swapParent- :: Path Abs Dir- -> Path Abs Dir- -> Path Abs t- -> IO (Path Abs t)- swapParent old new path = (new </>) `liftM`-#if MIN_VERSION_path(0,6,0)- stripProperPrefix old path-#else- stripDir old path-#endif- tdirs <- mapM (swapParent bsrc bdest) dirs- tfiles <- mapM (swapParent bsrc bdest) files- ensureDir bdest- mapM_ ensureDir tdirs- zipWithM_ copyFile files tfiles- when p $ do- ignoringIOErrors (copyPermissions bsrc bdest)- zipWithM_ (\s d -> ignoringIOErrors $ copyPermissions s d) dirs tdirs--------------------------------------------------------------------------------- Walking directory trees---- Recursive directory walk functionality, with a flexible API and avoidance--- of loops. Following are some notes on the design.------ Callback handler API:------ The callback handler interface is designed to be highly flexible. There are--- two possible alternative ways to control the traversal:------ * In the context of the parent dir, decide which subdirs to descend into.--- * In the context of the subdir, decide whether to traverse the subdir or not.------ We choose the first approach here since it is more flexible and can--- achieve everything that the second one can. The additional benefit with--- this is that we can use the parent dir context efficiently instead of--- each child looking at the parent context independently.------ To control which subdirs to descend we use a 'WalkExclude' API instead of--- a “WalkInclude” type of API so that the handlers cannot accidentally ask--- us to descend a dir which is not a subdir of the directory being walked.------ Avoiding Traversal Loops:------ There can be loops in the path being traversed due to subdirectory--- symlinks or filesystem corruptions can cause loops by creating directory--- hardlinks. Also, if the filesystem is changing while we are traversing--- then we might be going in loops due to the changes.------ We record the path we are coming from to detect the loops. If we end up--- traversing the same directory again we are in a loop.---- | Action returned by the traversal handler function. The action controls--- how the traversal will proceed.------ @since 1.2.0--data WalkAction- = WalkFinish -- ^ Finish the entire walk altogether- | WalkExclude [Path Abs Dir] -- ^ List of sub-directories to exclude from- -- descending- deriving (Eq, Show)---- | Traverse a directory tree using depth first pre-order traversal,--- calling a handler function at each directory node traversed. The absolute--- paths of the parent directory, sub-directories and the files in the--- directory are provided as arguments to the handler.------ The function is capable of detecting and avoiding traversal loops in the--- directory tree. Note that the traversal follows symlinks by default, an--- appropriate traversal handler can be used to avoid that when necessary.------ @since 1.2.0--walkDir- :: MonadIO m- => (Path Abs Dir -> [Path Abs Dir] -> [Path Abs File] -> m WalkAction)- -- ^ Handler (@dir -> subdirs -> files -> 'WalkAction'@)- -> Path b Dir- -- ^ Directory where traversal begins- -> m ()-walkDir handler topdir =- makeAbsolute topdir >>= walkAvoidLoop S.empty >> return ()- where- walkAvoidLoop traversed curdir = do- mRes <- checkLoop traversed curdir- case mRes of- Nothing -> return $ Just ()- Just traversed' -> walktree traversed' curdir-- -- use Maybe monad to abort any further traversal if any of the- -- handler calls returns WalkFinish- walktree traversed curdir = do- (subdirs, files) <- listDir curdir- action <- handler curdir subdirs files- case action of- WalkFinish -> return Nothing- WalkExclude xdirs ->- case subdirs \\ xdirs of- [] -> return $ Just ()- ds -> runMaybeT $ mapM_ (MaybeT . walkAvoidLoop traversed) ds-- checkLoop traversed dir = do- st <- liftIO $ P.getFileStatus (toFilePath dir)- let ufid = (P.deviceID st, P.fileID st)-- -- check for loop, have we already traversed this dir?- return $ if S.member ufid traversed- then Nothing- else Just (S.insert ufid traversed)---- | Similar to 'walkDir' but accepts a 'Monoid'-returning output writer as--- well. Values returned by the output writer invocations are accumulated--- and returned.------ Both, the descend handler as well as the output writer can be used for--- side effects but keep in mind that the output writer runs before the--- descend handler.------ @since 1.2.0--walkDirAccum- :: (MonadIO m, Monoid o)- => Maybe (Path Abs Dir -> [Path Abs Dir] -> [Path Abs File] -> m WalkAction)- -- ^ Descend handler (@dir -> subdirs -> files -> 'WalkAction'@),- -- descend the whole tree if omitted- -> (Path Abs Dir -> [Path Abs Dir] -> [Path Abs File] -> m o)- -- ^ Output writer (@dir -> subdirs -> files -> o@)- -> Path b Dir- -- ^ Directory where traversal begins- -> m o- -- ^ Accumulation of outputs generated by the output writer invocations-walkDirAccum dHandler writer topdir = execWriterT (walkDir handler topdir)- where- handler dir subdirs files = do- res <- lift $ writer dir subdirs files- tell res- case dHandler of- Just h -> lift $ h dir subdirs files- Nothing -> return (WalkExclude [])--------------------------------------------------------------------------------- Current working directory---- | Obtain the current working directory as an absolute path.------ In a multithreaded program, the current working directory is a global--- state shared among all threads of the process. Therefore, when performing--- filesystem operations from multiple threads, it is highly recommended to--- use absolute rather than relative paths (see: 'makeAbsolute').------ The operation may fail with:------ * 'HardwareFault'--- A physical I\/O error has occurred.--- @[EIO]@------ * 'isDoesNotExistError' or 'NoSuchThing'--- There is no path referring to the working directory.--- @[EPERM, ENOENT, ESTALE...]@------ * 'isPermissionError' or 'PermissionDenied'--- The process has insufficient privileges to perform the operation.--- @[EACCES]@------ * 'ResourceExhausted'--- Insufficient resources are available to perform the operation.------ * 'UnsupportedOperation'--- The operating system has no notion of current working directory.--getCurrentDir :: MonadIO m => m (Path Abs Dir)-getCurrentDir = liftIO $ D.getCurrentDirectory >>= parseAbsDir---- | Change the working directory to the given path.------ In a multithreaded program, the current working directory is a global--- state shared among all threads of the process. Therefore, when performing--- filesystem operations from multiple threads, it is highly recommended to--- use absolute rather than relative paths (see: 'makeAbsolute').------ The operation may fail with:------ * 'HardwareFault'--- A physical I\/O error has occurred.--- @[EIO]@------ * 'InvalidArgument'--- The operand is not a valid directory name.--- @[ENAMETOOLONG, ELOOP]@------ * 'isDoesNotExistError' or 'NoSuchThing'--- The directory does not exist.--- @[ENOENT, ENOTDIR]@------ * 'isPermissionError' or 'PermissionDenied'--- The process has insufficient privileges to perform the operation.--- @[EACCES]@------ * 'UnsupportedOperation'--- The operating system has no notion of current working directory, or the--- working directory cannot be dynamically changed.------ * 'InappropriateType'--- The path refers to an existing non-directory object.--- @[ENOTDIR]@--setCurrentDir :: MonadIO m => Path b Dir -> m ()-setCurrentDir = liftD D.setCurrentDirectory---- | Run an 'IO' action with the given working directory and restore the--- original working directory afterwards, even if the given action fails due--- to an exception.------ The operation may fail with the same exceptions as 'getCurrentDir' and--- 'setCurrentDir'.--withCurrentDir :: (MonadIO m, MonadMask m)- => Path b Dir -- ^ Directory to execute in- -> m a -- ^ Action to be executed- -> m a-withCurrentDir dir action =- bracket getCurrentDir setCurrentDir $ const (setCurrentDir dir >> action)--------------------------------------------------------------------------------- Pre-defined directories---- | Return the current user's home directory.------ The directory returned is expected to be writable by the current user,--- but note that it isn't generally considered good practice to store--- application-specific data here; use 'getAppUserDataDir' instead.------ On Unix, 'getHomeDir' returns the value of the @HOME@ environment--- variable. On Windows, the system is queried for a suitable path; a--- typical path might be @C:\/Users\//\<user\>/@.------ The operation may fail with:------ * 'UnsupportedOperation'--- The operating system has no notion of home directory.------ * 'isDoesNotExistError'--- The home directory for the current user does not exist, or--- cannot be found.--getHomeDir :: MonadIO m => m (Path Abs Dir)-getHomeDir = liftIO D.getHomeDirectory >>= resolveDir'---- | Obtain the path to a special directory for storing user-specific--- application data (traditional Unix location).------ The argument is usually the name of the application. Since it will be--- integrated into the path, it must consist of valid path characters.------ * On Unix-like systems, the path is @~\/./\<app\>/@.--- * On Windows, the path is @%APPDATA%\//\<app\>/@--- (e.g. @C:\/Users\//\<user\>/\/AppData\/Roaming\//\<app\>/@)------ Note: the directory may not actually exist, in which case you would need--- to create it. It is expected that the parent directory exists and is--- writable.------ The operation may fail with:------ * 'UnsupportedOperation'--- The operating system has no notion of application-specific data--- directory.------ * 'isDoesNotExistError'--- The home directory for the current user does not exist, or cannot be--- found.--getAppUserDataDir :: MonadIO m- => String -- ^ Name of application (used in path construction)- -> m (Path Abs Dir)-getAppUserDataDir = liftIO . (>>= parseAbsDir) . D.getAppUserDataDirectory---- | Return the current user's document directory.------ The directory returned is expected to be writable by the current user,--- but note that it isn't generally considered good practice to store--- application-specific data here; use 'getAppUserDataDir' instead.------ On Unix, 'getUserDocsDir' returns the value of the @HOME@ environment--- variable. On Windows, the system is queried for a suitable path; a--- typical path might be @C:\/Users\//\<user\>/\/Documents@.------ The operation may fail with:------ * 'UnsupportedOperation'--- The operating system has no notion of document directory.------ * 'isDoesNotExistError'--- The document directory for the current user does not exist, or--- cannot be found.--getUserDocsDir :: MonadIO m => m (Path Abs Dir)-getUserDocsDir = liftIO $ D.getUserDocumentsDirectory >>= parseAbsDir---- | Return the current directory for temporary files.------ On Unix, 'getTempDir' returns the value of the @TMPDIR@ environment--- variable or \"\/tmp\" if the variable isn\'t defined. On Windows, the--- function checks for the existence of environment variables in the--- following order and uses the first path found:------ *--- TMP environment variable.------ *--- TEMP environment variable.------ *--- USERPROFILE environment variable.------ *--- The Windows directory------ The operation may fail with:------ * 'UnsupportedOperation'--- The operating system has no notion of temporary directory.------ The function doesn't verify whether the path exists.--getTempDir :: MonadIO m => m (Path Abs Dir)-getTempDir = liftIO D.getTemporaryDirectory >>= resolveDir'--#if MIN_VERSION_directory(1,2,3)--- | Obtain the paths to special directories for storing user-specific--- application data, configuration, and cache files, conforming to the--- <http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html XDG Base Directory Specification>.--- Compared with 'getAppUserDataDir', this function provides a more--- fine-grained hierarchy as well as greater flexibility for the user.------ It also works on Windows, although in that case 'XdgData' and 'XdgConfig'--- will map to the same directory.------ Note: The directory may not actually exist, in which case you would need--- to create it with file mode @700@ (i.e. only accessible by the owner).------ Note also: this is a piece of conditional API, only available if--- @directory-1.2.3.0@ or later is used.------ @since 1.2.1--getXdgDir :: MonadIO m- => XdgDirectory -- ^ Which special directory- -> Maybe (Path Rel Dir)- -- ^ A relative path that is appended to the path; if 'Nothing', the- -- base path is returned- -> m (Path Abs Dir)-getXdgDir xdgDir suffix =- liftIO $ (D.getXdgDirectory xdgDir $ maybe "" toFilePath suffix) >>= parseAbsDir-#endif--------------------------------------------------------------------------------- Path transformation---- | Class of things ('Path's) that can be canonicalized, made absolute, and--- made relative to a some base directory.--class AnyPath path where-- -- | Type of absolute version of the given @path@.-- type AbsPath path :: *-- -- | Type of relative version of the given @path@.-- type RelPath path :: *-- -- | Make a path absolute and remove as many indirections from it as- -- possible. Indirections include the two special directories @.@ and- -- @..@, as well as any symbolic links. The input path need not point to- -- an existing file or directory.- --- -- __Note__: if you require only an absolute path, use 'makeAbsolute'- -- instead. Most programs need not care about whether a path contains- -- symbolic links.- --- -- Due to the fact that symbolic links are dependent on the state of the- -- existing filesystem, the function can only make a conservative,- -- best-effort attempt. Nevertheless, if the input path points to an- -- existing file or directory, then the output path shall also point to- -- the same file or directory.- --- -- Formally, symbolic links are removed from the longest prefix of the- -- path that still points to an existing file. The function is not atomic,- -- therefore concurrent changes in the filesystem may lead to incorrect- -- results.- --- -- (Despite the name, the function does not guarantee canonicity of the- -- returned path due to the presence of hard links, mount points, etc.)- --- -- /Known bug(s)/: on Windows, the function does not resolve symbolic- -- links.- --- -- Please note that before version 1.2.3.0 of the @directory@ package,- -- this function had unpredictable behavior on non-existent paths.-- canonicalizePath :: MonadIO m- => path- -> m (AbsPath path)-- -- | Make a path absolute by prepending the current directory (if it isn't- -- already absolute) and applying 'F.normalise' to the result.- --- -- If the path is already absolute, the operation never fails. Otherwise,- -- the operation may fail with the same exceptions as 'getCurrentDir'.-- makeAbsolute :: MonadIO m- => path- -> m (AbsPath path)-- -- | Make a path relative to a given directory.- --- -- @since 0.3.0-- makeRelative :: MonadThrow m- => Path Abs Dir -- ^ Base directory- -> path -- ^ Path that will be made relative to base directory- -> m (RelPath path)-- -- | Make a path relative to current working directory.- --- -- @since 0.3.0-- makeRelativeToCurrentDir :: MonadIO m- => path- -> m (RelPath path)--instance AnyPath (Path b File) where-- type AbsPath (Path b File) = Path Abs File- type RelPath (Path b File) = Path Rel File-- canonicalizePath = liftD $ D.canonicalizePath >=> parseAbsFile- makeAbsolute = liftD $ D.makeAbsolute >=> parseAbsFile- makeRelative b p = parseRelFile (F.makeRelative (toFilePath b) (toFilePath p))- makeRelativeToCurrentDir p = liftIO $ getCurrentDir >>= flip makeRelative p--instance AnyPath (Path b Dir) where-- type AbsPath (Path b Dir) = Path Abs Dir- type RelPath (Path b Dir) = Path Rel Dir-- canonicalizePath = liftD D.canonicalizePath >=> liftIO . parseAbsDir- makeAbsolute = liftD D.makeAbsolute >=> liftIO . parseAbsDir- makeRelative b p = parseRelDir (F.makeRelative (toFilePath b) (toFilePath p))- makeRelativeToCurrentDir p = liftIO $ getCurrentDir >>= flip makeRelative p---- | Append stringly-typed path to an absolute path and then canonicalize--- it.------ @since 0.3.0--resolveFile :: MonadIO m- => Path Abs Dir -- ^ Base directory- -> FilePath -- ^ Path to resolve- -> m (Path Abs File)-resolveFile b p = liftIO $ D.canonicalizePath (toFilePath b F.</> p) >>= parseAbsFile---- | The same as 'resolveFile', but uses current working directory.------ @since 0.3.0--resolveFile' :: MonadIO m- => FilePath -- ^ Path to resolve- -> m (Path Abs File)-resolveFile' p = getCurrentDir >>= flip resolveFile p---- | The same as 'resolveFile', but for directories.------ @since 0.3.0--resolveDir :: MonadIO m- => Path Abs Dir -- ^ Base directory- -> FilePath -- ^ Path to resolve- -> m (Path Abs Dir)-resolveDir b p = liftIO $ D.canonicalizePath (toFilePath b F.</> p) >>= parseAbsDir---- | The same as 'resolveDir', but uses current working directory.------ @since 0.3.0--resolveDir' :: MonadIO m- => FilePath -- ^ Path to resolve- -> m (Path Abs Dir)-resolveDir' p = getCurrentDir >>= flip resolveDir p--------------------------------------------------------------------------------- Actions on files---- | @'removeFile' file@ removes the directory entry for an existing file--- @file@, where @file@ is not itself a directory. The implementation may--- specify additional constraints which must be satisfied before a file can--- be removed (e.g. the file may not be in use by other processes).------ The operation may fail with:------ * 'HardwareFault'--- A physical I\/O error has occurred.--- @[EIO]@------ * 'InvalidArgument'--- The operand is not a valid file name.--- @[ENAMETOOLONG, ELOOP]@------ * 'isDoesNotExistError' \/ 'NoSuchThing'--- The file does not exist.--- @[ENOENT, ENOTDIR]@------ * 'isPermissionError' \/ 'PermissionDenied'--- The process has insufficient privileges to perform the operation.--- @[EROFS, EACCES, EPERM]@------ * 'UnsatisfiedConstraints'--- Implementation-dependent constraints are not satisfied.--- @[EBUSY]@------ * 'InappropriateType'--- The operand refers to an existing directory.--- @[EPERM, EINVAL]@--removeFile :: MonadIO m => Path b File -> m ()-removeFile = liftD D.removeFile---- | @'renameFile' old new@ changes the name of an existing file system--- object from /old/ to /new/. If the /new/ object already exists, it is--- atomically replaced by the /old/ object. Neither path may refer to an--- existing directory. A conformant implementation need not support renaming--- files in all situations (e.g. renaming across different physical--- devices), but the constraints must be documented.------ The operation may fail with:------ * 'HardwareFault'--- A physical I\/O error has occurred.--- @[EIO]@------ * 'InvalidArgument'--- Either operand is not a valid file name.--- @[ENAMETOOLONG, ELOOP]@------ * 'isDoesNotExistError' \/ 'NoSuchThing'--- The original file does not exist, or there is no path to the target.--- @[ENOENT, ENOTDIR]@------ * 'isPermissionError' \/ 'PermissionDenied'--- The process has insufficient privileges to perform the operation.--- @[EROFS, EACCES, EPERM]@------ * 'ResourceExhausted'--- Insufficient resources are available to perform the operation.--- @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@------ * 'UnsatisfiedConstraints'--- Implementation-dependent constraints are not satisfied.--- @[EBUSY]@------ * 'UnsupportedOperation'--- The implementation does not support renaming in this situation.--- @[EXDEV]@------ * 'InappropriateType'--- Either path refers to an existing directory.--- @[ENOTDIR, EISDIR, EINVAL, EEXIST, ENOTEMPTY]@--renameFile :: MonadIO m- => Path b0 File -- ^ Original location- -> Path b1 File -- ^ New location- -> m ()-renameFile = liftD2 D.renameFile---- | @'copyFile' old new@ copies the existing file from @old@ to @new@. If--- the @new@ file already exists, it is atomically replaced by the @old@--- file. Neither path may refer to an existing directory. The permissions of--- @old@ are copied to @new@, if possible.--copyFile :: MonadIO m- => Path b0 File -- ^ Original location- -> Path b1 File -- ^ Where to put copy- -> m ()-copyFile = liftD2 D.copyFile---- | Given an executable file name, search for such file in the directories--- listed in system @PATH@. The returned value is the path to the found--- executable or 'Nothing' if an executable with the given name was not--- found. For example ('findExecutable' \"ghc\") gives you the path to GHC.------ The path returned by 'findExecutable' corresponds to the program that--- would be executed by 'System.Process.createProcess' when passed the same--- string (as a RawCommand, not a ShellCommand).------ On Windows, 'findExecutable' calls the Win32 function 'SearchPath', which--- may search other places before checking the directories in @PATH@. Where--- it actually searches depends on registry settings, but notably includes--- the directory containing the current executable. See--- <http://msdn.microsoft.com/en-us/library/aa365527.aspx> for more details.--findExecutable :: MonadIO m- => Path Rel File -- ^ Executable file name- -> m (Maybe (Path Abs File)) -- ^ Path to found executable-findExecutable = liftM (>>= parseAbsFile) . liftD D.findExecutable---- | Search through the given set of directories for the given file.--findFile :: MonadIO m- => [Path b Dir] -- ^ Set of directories to search in- -> Path Rel File -- ^ Filename of interest- -> m (Maybe (Path Abs File)) -- ^ Absolute path to file (if found)-findFile [] _ = return Nothing-findFile (d:ds) file = do- bfile <- (</> file) `liftM` makeAbsolute d- exist <- doesFileExist bfile- if exist- then return (Just bfile)- else findFile ds file---- | Search through the given set of directories for the given file and--- return a list of paths where the given file exists.--findFiles :: MonadIO m- => [Path b Dir] -- ^ Set of directories to search in- -> Path Rel File -- ^ Filename of interest- -> m [Path Abs File] -- ^ Absolute paths to all found files-findFiles = findFilesWith (const (return True))---- | Search through the given set of directories for the given file and with--- the given property (usually permissions) and return a list of paths where--- the given file exists and has the property.--findFilesWith :: MonadIO m- => (Path Abs File -> m Bool) -- ^ How to test the files- -> [Path b Dir] -- ^ Set of directories to search in- -> Path Rel File -- ^ Filename of interest- -> m [Path Abs File] -- ^ Absolute paths to all found files-findFilesWith _ [] _ = return []-findFilesWith f (d:ds) file = do- bfile <- (</> file) `liftM` makeAbsolute d- exist <- doesFileExist file- b <- if exist then f bfile else return False- if b- then (bfile:) `liftM` findFilesWith f ds file- else findFilesWith f ds file--------------------------------------------------------------------------------- Symbolic links---- | Check if the given path is a symbolic link.------ @since 1.3.0--isSymlink :: MonadIO m => Path b t -> m Bool-isSymlink p = liftIO $ liftM P.isSymbolicLink (P.getSymbolicLinkStatus path)- where- -- NOTE: To be able to correctly check whether it is a symlink or not we- -- have to drop the trailing separator from the dir path.- path = F.dropTrailingPathSeparator (toFilePath p)--------------------------------------------------------------------------------- Temporary files and directories---- | Use a temporary file that doesn't already exist.------ Creates a new temporary file inside the given directory, making use of--- the template. The temporary file is deleted after use.------ @since 0.2.0--withTempFile :: (MonadIO m, MonadMask m)- => Path b Dir -- ^ Directory to create the file in- -> String -- ^ File name template, see 'openTempFile'- -> (Path Abs File -> Handle -> m a) -- ^ Callback that can use the file- -> m a-withTempFile path t action = do- apath <- makeAbsolute path- T.withTempFile (toFilePath apath) t $ \file h ->- parseAbsFile file >>= flip action h---- | Create and use a temporary directory.------ Creates a new temporary directory inside the given directory, making use--- of the template. The temporary directory is deleted after use.------ @since 0.2.0--withTempDir :: (MonadIO m, MonadMask m)- => Path b Dir -- ^ Directory to create the file in- -> String -- ^ Directory name template, see 'openTempFile'- -> (Path Abs Dir -> m a) -- ^ Callback that can use the directory- -> m a-withTempDir path t action = do- apath <- makeAbsolute path- T.withTempDirectory (toFilePath apath) t (parseAbsDir >=> action)---- | Create and use a temporary file in the system standard temporary--- directory.------ Behaves exactly the same as 'withTempFile', except that the parent--- temporary directory will be that returned by 'getTempDir'.------ @since 0.2.0--withSystemTempFile :: (MonadIO m, MonadMask m)- => String -- ^ File name template, see 'openTempFile'- -> (Path Abs File -> Handle -> m a) -- ^ Callback that can use the file- -> m a-withSystemTempFile t action = getTempDir >>= \path ->- withTempFile path t action---- | Create and use a temporary directory in the system standard temporary--- directory.------ Behaves exactly the same as 'withTempDir', except that the parent--- temporary directory will be that returned by 'getTempDir'.------ @since 0.2.0--withSystemTempDir :: (MonadIO m, MonadMask m)- => String -- ^ Directory name template, see 'openTempFile'- -> (Path Abs Dir -> m a) -- ^ Callback that can use the directory- -> m a-withSystemTempDir t action = getTempDir >>= \path ->- withTempDir path t action---- | The function creates a temporary file in @rw@ mode. The created file--- isn't deleted automatically, so you need to delete it manually.------ The file is created with permissions such that only the current user can--- read\/write it.------ With some exceptions (see below), the file will be created securely in--- the sense that an attacker should not be able to cause openTempFile to--- overwrite another file on the filesystem using your credentials, by--- putting symbolic links (on Unix) in the place where the temporary file is--- to be created. On Unix the @O_CREAT@ and @O_EXCL@ flags are used to--- prevent this attack, but note that @O_EXCL@ is sometimes not supported on--- NFS filesystems, so if you rely on this behaviour it is best to use local--- filesystems only.------ @since 0.2.0--openTempFile :: MonadIO m- => Path b Dir -- ^ Directory to create file in- -> String- -- ^ File name template; if the template is "foo.ext" then the created- -- file will be @\"fooXXX.ext\"@ where @XXX@ is some random number- -> m (Path Abs File, Handle) -- ^ Name of created file and its 'Handle'-openTempFile path t = liftIO $ do- apath <- makeAbsolute path- (tfile, h) <- liftD2' T.openTempFile apath t- (,h) `liftM` parseAbsFile tfile---- | Like 'openTempFile', but opens the file in binary mode. On Windows,--- reading a file in text mode (which is the default) will translate @CRLF@--- to @LF@, and writing will translate @LF@ to @CRLF@. This is usually what--- you want with text files. With binary files this is undesirable; also, as--- usual under Microsoft operating systems, text mode treats control-Z as--- EOF. Binary mode turns off all special treatment of end-of-line and--- end-of-file characters.------ @since 0.2.0--openBinaryTempFile :: MonadIO m- => Path b Dir -- ^ Directory to create file in- -> String -- ^ File name template, see 'openTempFile'- -> m (Path Abs File, Handle) -- ^ Name of created file and its 'Handle'-openBinaryTempFile path t = liftIO $ do- apath <- makeAbsolute path- (tfile, h) <- liftD2' T.openBinaryTempFile apath t- (,h) `liftM` parseAbsFile tfile---- | Create a temporary directory. The created directory isn't deleted--- automatically, so you need to delete it manually.------ The directory is created with permissions such that only the current user--- can read\/write it.------ @since 0.2.0--createTempDir :: MonadIO m- => Path b Dir -- ^ Directory to create file in- -> String -- ^ Directory name template, see 'openTempFile'- -> m (Path Abs Dir) -- ^ Name of created temporary directory-createTempDir path t = liftIO $ makeAbsolute path >>= \apath ->- liftD2' T.createTempDirectory apath t >>= parseAbsDir--------------------------------------------------------------------------------- Existence tests---- | The operation 'doesFileExist' returns 'True' if the argument file--- exists and is not a directory, and 'False' otherwise.--doesFileExist :: MonadIO m => Path b File -> m Bool-doesFileExist = liftD D.doesFileExist---- | The operation 'doesDirExist' returns 'True' if the argument file exists--- and is either a directory or a symbolic link to a directory, and 'False'--- otherwise.--doesDirExist :: MonadIO m => Path b Dir -> m Bool-doesDirExist = liftD D.doesDirectoryExist---- | Check if there is a file or directory on specified path.--isLocationOccupied :: MonadIO m => Path b t -> m Bool-isLocationOccupied path = do- let fp = toFilePath path- file <- liftIO (D.doesFileExist fp)- dir <- liftIO (D.doesDirectoryExist fp)- return (file || dir)---- | If argument of the function throws a--- 'System.IO.Error.doesNotExistErrorType', 'Nothing' is returned (other--- exceptions propagate). Otherwise the result is returned inside a 'Just'.------ @since 0.3.0--forgivingAbsence :: (MonadIO m, MonadCatch m) => m a -> m (Maybe a)-forgivingAbsence f = catchIf isDoesNotExistError- (Just `liftM` f)- (const $ return Nothing)---- | The same as 'forgivingAbsence', but ignores result.------ @since 0.3.1--ignoringAbsence :: (MonadIO m, MonadCatch m) => m a -> m ()-ignoringAbsence = liftM (const ()) . forgivingAbsence--------------------------------------------------------------------------------- Permissions---- | The 'getPermissions' operation returns the permissions for the file or--- directory.------ The operation may fail with:------ * 'isPermissionError' if the user is not permitted to access--- the permissions; or------ * 'isDoesNotExistError' if the file or directory does not exist.--getPermissions :: MonadIO m => Path b t -> m D.Permissions-getPermissions = liftD D.getPermissions---- | The 'setPermissions' operation sets the permissions for the file or--- directory.------ The operation may fail with:------ * 'isPermissionError' if the user is not permitted to set--- the permissions; or------ * 'isDoesNotExistError' if the file or directory does not exist.--setPermissions :: MonadIO m => Path b t -> D.Permissions -> m ()-setPermissions = liftD2' D.setPermissions---- | Set permissions for the object found on second given path so they match--- permissions of the object on the first path.--copyPermissions :: MonadIO m- => Path b0 t0 -- ^ From where to copy- -> Path b1 t1 -- ^ What to modify- -> m ()-copyPermissions = liftD2 D.copyPermissions--------------------------------------------------------------------------------- Timestamps--#if MIN_VERSION_directory(1,2,3)---- | Obtain the time at which the file or directory was last accessed.------ The operation may fail with:------ * 'isPermissionError' if the user is not permitted to read--- the access time; or------ * 'isDoesNotExistError' if the file or directory does not exist.------ Caveat for POSIX systems: This function returns a timestamp with--- sub-second resolution only if this package is compiled against--- @unix-2.6.0.0@ or later and the underlying filesystem supports them.------ Note: this is a piece of conditional API, only available if--- @directory-1.2.3.0@ or later is used.--getAccessTime :: MonadIO m => Path b t -> m UTCTime-getAccessTime = liftD D.getAccessTime---- | Change the time at which the file or directory was last accessed.------ The operation may fail with:------ * 'isPermissionError' if the user is not permitted to alter the--- access time; or------ * 'isDoesNotExistError' if the file or directory does not exist.------ Some caveats for POSIX systems:------ * Not all systems support @utimensat@, in which case the function can--- only emulate the behavior by reading the modification time and then--- setting both the access and modification times together. On systems--- where @utimensat@ is supported, the access time is set atomically with--- nanosecond precision.------ * If compiled against a version of @unix@ prior to @2.7.0.0@, the--- function would not be able to set timestamps with sub-second--- resolution. In this case, there would also be loss of precision in the--- modification time.------ Note: this is a piece of conditional API, only available if--- @directory-1.2.3.0@ or later is used.--setAccessTime :: MonadIO m => Path b t -> UTCTime -> m ()-setAccessTime = liftD2' D.setAccessTime---- | Change the time at which the file or directory was last modified.------ The operation may fail with:------ * 'isPermissionError' if the user is not permitted to alter the--- modification time; or------ * 'isDoesNotExistError' if the file or directory does not exist.------ Some caveats for POSIX systems:------ * Not all systems support @utimensat@, in which case the function can--- only emulate the behavior by reading the access time and then setting--- both the access and modification times together. On systems where--- @utimensat@ is supported, the modification time is set atomically with--- nanosecond precision.------ * If compiled against a version of @unix@ prior to @2.7.0.0@, the--- function would not be able to set timestamps with sub-second--- resolution. In this case, there would also be loss of precision in the--- access time.------ Note: this is a piece of conditional API, only available if--- @directory-1.2.3.0@ or later is used.--setModificationTime :: MonadIO m => Path b t -> UTCTime -> m ()-setModificationTime = liftD2' D.setModificationTime-#endif---- | Obtain the time at which the file or directory was last modified.------ The operation may fail with:------ * 'isPermissionError' if the user is not permitted to read--- the modification time; or------ * 'isDoesNotExistError' if the file or directory does not exist.------ Caveat for POSIX systems: This function returns a timestamp with--- sub-second resolution only if this package is compiled against--- @unix-2.6.0.0@ or later and the underlying filesystem supports them.--getModificationTime :: MonadIO m => Path b t -> m UTCTime-getModificationTime = liftD D.getModificationTime--------------------------------------------------------------------------------- Helpers---- | Lift action in 'IO' that takes 'FilePath' into action in slightly more--- abstract monad that takes 'Path'.--liftD :: MonadIO m- => (FilePath -> IO a) -- ^ Original action- -> Path b t -- ^ 'Path' argument- -> m a -- ^ Lifted action-liftD m = liftIO . m . toFilePath-{-# INLINE liftD #-}---- | Similar to 'liftD' for functions with arity 2.--liftD2 :: MonadIO m- => (FilePath -> FilePath -> IO a) -- ^ Original action- -> Path b0 t0 -- ^ First 'Path' argument- -> Path b1 t1 -- ^ Second 'Path' argument- -> m a-liftD2 m a b = liftIO $ m (toFilePath a) (toFilePath b)-{-# INLINE liftD2 #-}---- | Similar to 'liftD2', but allows to pass second argument of arbitrary--- type.--liftD2' :: MonadIO m- => (FilePath -> v -> IO a) -- ^ Original action- -> Path b t -- ^ First 'Path' argument- -> v -- ^ Second argument- -> m a-liftD2' m a v = liftIO $ m (toFilePath a) v-{-# INLINE liftD2' #-}---- | Perform an action ignoring IO exceptions it may throw.--ignoringIOErrors :: IO () -> IO ()-ignoringIOErrors ioe = ioe `catch` handler- where- handler :: Monad m => IOError -> m ()+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- Module : Path.IO+-- Copyright : © 2016–present Mark Karpov+-- License : BSD 3 clause+--+-- Maintainer : Mark Karpov <markkarpov92@gmail.com>+-- Stability : experimental+-- Portability : portable+--+-- This module provides an interface to "System.Directory" for users of the+-- "Path" module. It also implements some extra functionality like recursive+-- scanning and copying of directories, working with temporary+-- files\/directories, etc.+module Path.IO+ ( -- * Actions on directories+ createDir,+ createDirIfMissing,+ ensureDir,+ removeDir,+ removeDirRecur,+ removePathForcibly,+ renameDir,+ renamePath,+ listDir,+ listDirRel,+ listDirRecur,+ listDirRecurRel,+ copyDirRecur,+ copyDirRecur',++ -- ** Walking directory trees+ WalkAction (..),+ walkDir,+ walkDirRel,+ walkDirAccum,+ walkDirAccumRel,++ -- ** Current working directory+ getCurrentDir,+ setCurrentDir,+ withCurrentDir,++ -- * Pre-defined directories+ getHomeDir,+ getAppUserDataDir,+ getUserDocsDir,+ getTempDir,+ D.XdgDirectory (..),+ getXdgDir,+ D.XdgDirectoryList (..),+ getXdgDirList,++ -- * Path transformation+ AnyPath (..),+ resolveFile,+ resolveFile',+ resolveDir,+ resolveDir',++ -- * Actions on files+ removeFile,+ renameFile,+ copyFile,+ getFileSize,+ findExecutable,+ findFile,+ findFiles,+ findFilesWith,++ -- * Symbolic links+ createFileLink,+ createDirLink,+ removeDirLink,+ getSymlinkTarget,+ isSymlink,++ -- * Temporary files and directories+ withTempFile,+ withTempDir,+ withSystemTempFile,+ withSystemTempDir,+ openTempFile,+ openBinaryTempFile,+ createTempDir,++ -- * Existence tests+ doesPathExist,+ doesFileExist,+ doesDirExist,+ isLocationOccupied,+ forgivingAbsence,+ ignoringAbsence,++ -- * Permissions+ D.Permissions,+ D.emptyPermissions,+ D.readable,+ D.writable,+ D.executable,+ D.searchable,+ D.setOwnerReadable,+ D.setOwnerWritable,+ D.setOwnerExecutable,+ D.setOwnerSearchable,+ getPermissions,+ setPermissions,+ copyPermissions,++ -- * Timestamps+ getAccessTime,+ setAccessTime,+ setModificationTime,+ getModificationTime,+ )+where++import Control.Arrow ((***))+import Control.Monad+import Control.Monad.Catch+import Control.Monad.IO.Class (MonadIO (..))+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)+import Control.Monad.Trans.Writer.Strict (WriterT, execWriterT, tell)+import Data.DList qualified as DList+import Data.Either (lefts, rights)+import Data.Kind (Type)+import Data.List ((\\))+import Data.Set qualified as S+import Data.Time (UTCTime)+import Path+import System.Directory qualified as D+import System.FilePath qualified as F+import System.IO (Handle)+import System.IO.Error (isDoesNotExistError)+import System.IO.Temp qualified as T+import System.PosixCompat.Files qualified as P++----------------------------------------------------------------------------+-- Actions on directories++-- | @'createDir' dir@ creates a new directory @dir@ which is initially+-- empty, or as near to empty as the operating system allows.+--+-- The operation may fail with:+--+-- * 'isPermissionError' \/ 'PermissionDenied'+-- The process has insufficient privileges to perform the operation.+-- @[EROFS, EACCES]@+--+-- * 'isAlreadyExistsError' \/ 'AlreadyExists'+-- The operand refers to a directory that already exists.+-- @ [EEXIST]@+--+-- * 'HardwareFault'+-- A physical I\/O error has occurred.+-- @[EIO]@+--+-- * 'InvalidArgument'+-- The operand is not a valid directory name.+-- @[ENAMETOOLONG, ELOOP]@+--+-- * 'NoSuchThing'+-- There is no path to the directory.+-- @[ENOENT, ENOTDIR]@+--+-- * 'ResourceExhausted' Insufficient resources (virtual memory, process+-- file descriptors, physical disk space, etc.) are available to perform the+-- operation. @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@+--+-- * 'InappropriateType'+-- The path refers to an existing non-directory object.+-- @[EEXIST]@+createDir :: (MonadIO m) => Path b Dir -> m ()+createDir = liftD D.createDirectory++-- | @'createDirIfMissing' parents dir@ creates a new directory @dir@ if it+-- doesn't exist. If the first argument is 'True' the function will also+-- create all parent directories if they are missing.+createDirIfMissing ::+ (MonadIO m) =>+ -- | Create its parents too?+ Bool ->+ -- | The path to the directory you want to make+ Path b Dir ->+ m ()+createDirIfMissing p = liftD (D.createDirectoryIfMissing p)++-- | Ensure that a directory exists creating it and its parent directories+-- if necessary. This is just a handy shortcut:+--+-- > ensureDir = createDirIfMissing True+--+-- @since 0.3.1+ensureDir :: (MonadIO m) => Path b Dir -> m ()+ensureDir = createDirIfMissing True++-- | @'removeDir' dir@ removes an existing directory @dir@. The+-- implementation may specify additional constraints which must be satisfied+-- before a directory can be removed (e.g. the directory has to be empty, or+-- may not be in use by other processes). It is not legal for an+-- implementation to partially remove a directory unless the entire+-- directory is removed. A conformant implementation need not support+-- directory removal in all situations (e.g. removal of the root directory).+--+-- The operation may fail with:+--+-- * 'HardwareFault'+-- A physical I\/O error has occurred.+-- @[EIO]@+--+-- * 'InvalidArgument'+-- The operand is not a valid directory name.+-- @[ENAMETOOLONG, ELOOP]@+--+-- * 'isDoesNotExistError' \/ 'NoSuchThing'+-- The directory does not exist.+-- @[ENOENT, ENOTDIR]@+--+-- * 'isPermissionError' \/ 'PermissionDenied'+-- The process has insufficient privileges to perform the operation.+-- @[EROFS, EACCES, EPERM]@+--+-- * 'UnsatisfiedConstraints'+-- Implementation-dependent constraints are not satisfied.+-- @[EBUSY, ENOTEMPTY, EEXIST]@+--+-- * 'UnsupportedOperation'+-- The implementation does not support removal in this situation.+-- @[EINVAL]@+--+-- * 'InappropriateType'+-- The operand refers to an existing non-directory object.+-- @[ENOTDIR]@+removeDir :: (MonadIO m) => Path b Dir -> m ()+removeDir = liftD D.removeDirectory++-- | @'removeDirRecur' dir@ removes an existing directory @dir@ together+-- with its contents and sub-directories. Within this directory, symbolic+-- links are removed without affecting their targets.+removeDirRecur :: (MonadIO m) => Path b Dir -> m ()+removeDirRecur = liftD D.removeDirectoryRecursive++-- | Remove a file or directory at /path/ together with its contents and+-- subdirectories. Symbolic links are removed without affecting their+-- targets. If the path does not exist, nothing happens.+--+-- Unlike other removal functions, this function will also attempt to delete+-- files marked as read-only or otherwise made unremovable due to permissions.+-- As a result, if the removal is incomplete, the permissions or attributes on+-- the remaining files may be altered. If there are hard links in the+-- directory, then permissions on all related hard links may be altered.+--+-- If an entry within the directory vanishes while @removePathForcibly@ is+-- running, it is silently ignored.+--+-- If an exception occurs while removing an entry, @removePathForcibly@ will+-- still try to remove as many entries as it can before failing with an+-- exception. The first exception that it encountered is re-thrown.+--+-- @since 1.7.0+removePathForcibly :: (MonadIO m) => Path b t -> m ()+removePathForcibly = liftD D.removePathForcibly++-- | @'renameDir' old new@ changes the name of an existing directory from+-- @old@ to @new@. If the @new@ directory already exists, it is atomically+-- replaced by the @old@ directory. If the @new@ directory is neither the+-- @old@ directory nor an alias of the @old@ directory, it is removed as if+-- by 'removeDir'. A conformant implementation need not support renaming+-- directories in all situations (e.g. renaming to an existing directory, or+-- across different physical devices), but the constraints must be+-- documented.+--+-- On Win32 platforms, @renameDir@ fails if the @new@ directory already+-- exists.+--+-- The operation may fail with:+--+-- * 'HardwareFault'+-- A physical I\/O error has occurred.+-- @[EIO]@+--+-- * 'InvalidArgument'+-- Either operand is not a valid directory name.+-- @[ENAMETOOLONG, ELOOP]@+--+-- * 'isDoesNotExistError' \/ 'NoSuchThing'+-- The original directory does not exist, or there is no path to the target.+-- @[ENOENT, ENOTDIR]@+--+-- * 'isPermissionError' \/ 'PermissionDenied'+-- The process has insufficient privileges to perform the operation.+-- @[EROFS, EACCES, EPERM]@+--+-- * 'ResourceExhausted'+-- Insufficient resources are available to perform the operation.+-- @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@+--+-- * 'UnsatisfiedConstraints'+-- Implementation-dependent constraints are not satisfied.+-- @[EBUSY, ENOTEMPTY, EEXIST]@+--+-- * 'UnsupportedOperation'+-- The implementation does not support renaming in this situation.+-- @[EINVAL, EXDEV]@+--+-- * 'InappropriateType'+-- Either path refers to an existing non-directory object.+-- @[ENOTDIR, EISDIR]@+renameDir ::+ (MonadIO m) =>+ -- | Old name+ Path b0 Dir ->+ -- | New name+ Path b1 Dir ->+ m ()+renameDir = liftD2 D.renameDirectory++-- | Rename a file or directory. If the destination path already exists, it+-- is replaced atomically. The destination path must not point to an existing+-- directory. A conformant implementation need not support renaming files in+-- all situations (e.g. renaming across different physical devices), but the+-- constraints must be documented.+--+-- The operation may fail with:+--+-- * @HardwareFault@+-- A physical I\/O error has occurred.+-- @[EIO]@+--+-- * @InvalidArgument@+-- Either operand is not a valid file name.+-- @[ENAMETOOLONG, ELOOP]@+--+-- * 'isDoesNotExistError'+-- The original file does not exist, or there is no path to the target.+-- @[ENOENT, ENOTDIR]@+--+-- * 'isPermissionError'+-- The process has insufficient privileges to perform the operation.+-- @[EROFS, EACCES, EPERM]@+--+-- * 'System.IO.isFullError'+-- Insufficient resources are available to perform the operation.+-- @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@+--+-- * @UnsatisfiedConstraints@+-- Implementation-dependent constraints are not satisfied.+-- @[EBUSY]@+--+-- * @UnsupportedOperation@+-- The implementation does not support renaming in this situation.+-- @[EXDEV]@+--+-- * @InappropriateType@+-- Either the destination path refers to an existing directory, or one of the+-- parent segments in the destination path is not a directory.+-- @[ENOTDIR, EISDIR, EINVAL, EEXIST, ENOTEMPTY]@+--+-- @since 1.7.0+renamePath :: (MonadIO m) => Path b0 t -> Path b1 t -> m ()+renamePath = liftD2 D.renamePath++-- | @'listDir' dir@ returns a list of /all/ entries in @dir@ without the+-- special entries (@.@ and @..@). Entries are not sorted.+--+-- The operation may fail with:+--+-- * 'HardwareFault'+-- A physical I\/O error has occurred.+-- @[EIO]@+--+-- * 'InvalidArgument'+-- The operand is not a valid directory name.+-- @[ENAMETOOLONG, ELOOP]@+--+-- * 'isDoesNotExistError' \/ 'NoSuchThing'+-- The directory does not exist.+-- @[ENOENT, ENOTDIR]@+--+-- * 'isPermissionError' \/ 'PermissionDenied'+-- The process has insufficient privileges to perform the operation.+-- @[EACCES]@+--+-- * 'ResourceExhausted'+-- Insufficient resources are available to perform the operation.+-- @[EMFILE, ENFILE]@+--+-- * 'InappropriateType'+-- The path refers to an existing non-directory object.+-- @[ENOTDIR]@+listDir ::+ (MonadIO m) =>+ -- | Directory to list+ Path b Dir ->+ -- | Sub-directories and files+ m ([Path Abs Dir], [Path Abs File])+listDir path = do+ bpath <- makeAbsolute path+ (subdirs, files) <- listDirRel bpath+ return+ ( (bpath </>) <$> subdirs,+ (bpath </>) <$> files+ )++-- | The same as 'listDir' but returns relative paths.+--+-- @since 1.4.0+listDirRel ::+ (MonadIO m) =>+ -- | Directory to list+ Path b Dir ->+ -- | Sub-directories and files+ m ([Path Rel Dir], [Path Rel File])+listDirRel path = liftIO $ do+ raw <- liftD D.getDirectoryContents path+ items <- forM (raw \\ [".", ".."]) $ \item -> do+ isDir <- liftIO (D.doesDirectoryExist $ toFilePath path F.</> item)+ if isDir+ then Left <$> parseRelDir item+ else Right <$> parseRelFile item+ return (lefts items, rights items)++-- | Similar to 'listDir', but recursively traverses every sub-directory+-- /excluding symbolic links/, and returns all files and directories found.+-- This can fail with the same exceptions as 'listDir'.+--+-- __Note__: before version /1.3.0/, this function followed symlinks.+listDirRecur ::+ (MonadIO m) =>+ -- | Directory to list+ Path b Dir ->+ -- | Sub-directories and files+ m ([Path Abs Dir], [Path Abs File])+listDirRecur dir =+ (DList.toList *** DList.toList)+ <$> walkDirAccum (Just excludeSymlinks) writer dir+ where+ excludeSymlinks _ subdirs _ =+ WalkExclude <$> filterM isSymlink subdirs+ writer _ ds fs =+ return+ ( DList.fromList ds,+ DList.fromList fs+ )++-- | The same as 'listDirRecur' but returns paths that are relative to the+-- given directory.+--+-- @since 1.4.2+listDirRecurRel ::+ (MonadIO m) =>+ -- | Directory to list+ Path b Dir ->+ -- | Sub-directories and files+ m ([Path Rel Dir], [Path Rel File])+listDirRecurRel dir =+ (DList.toList *** DList.toList)+ <$> walkDirAccumRel (Just excludeSymlinks) writer dir+ where+ excludeSymlinks tdir subdirs _ =+ WalkExclude <$> filterM (isSymlink . (dir </>) . (tdir </>)) subdirs+ writer tdir ds fs =+ return+ ( DList.fromList ((tdir </>) <$> ds),+ DList.fromList ((tdir </>) <$> fs)+ )++-- | Copies a directory recursively. It /does not/ follow symbolic links and+-- preserves permissions when possible. If the destination directory already+-- exists, new files and sub-directories complement its structure, possibly+-- overwriting old files if they happen to have the same name as the new+-- ones.+--+-- __Note__: before version /1.3.0/, this function followed symlinks.+--+-- __Note__: before version /1.6.0/, the function created empty directories+-- in the destination directory when the source directory contained+-- directory symlinks. The symlinked directories were not recursively+-- traversed. It also copied symlinked files creating normal regular files+-- in the target directory as the result. This was fixed in the version+-- /1.6.0/ so that the function now behaves much like the @cp@ utility, not+-- traversing symlinked directories, but recreating symlinks in the target+-- directory according to their targets in the source directory.+copyDirRecur ::+ (MonadIO m) =>+ -- | Source+ Path b0 Dir ->+ -- | Destination+ Path b1 Dir ->+ m ()+copyDirRecur = copyDirRecurGen True++-- | The same as 'copyDirRecur', but it /does not/ preserve directory+-- permissions. This may be useful, for example, if the directory you want+-- to copy is “read-only”, but you want your copy to be editable.+--+-- @since 1.1.0+--+-- __Note__: before version /1.3.0/, this function followed symlinks.+--+-- __Note__: before version /1.6.0/, the function created empty directories+-- in the destination directory when the source directory contained+-- directory symlinks. The symlinked directories were not recursively+-- traversed. It also copied symlinked files creating normal regular files+-- in the target directory as the result. This was fixed in the version+-- /1.6.0/ so that the function now behaves much like the @cp@ utility, not+-- traversing symlinked directories, but recreating symlinks in the target+-- directory according to their targets in the source directory.+copyDirRecur' ::+ (MonadIO m) =>+ -- | Source+ Path b0 Dir ->+ -- | Destination+ Path b1 Dir ->+ m ()+copyDirRecur' = copyDirRecurGen False++-- | Generic version of 'copyDirRecur'. The first argument controls whether+-- to preserve directory permissions or not. /Does not/ follow symbolic+-- links. Internal function.+copyDirRecurGen ::+ (MonadIO m) =>+ -- | Should we preserve directory permissions?+ Bool ->+ -- | Source+ Path b0 Dir ->+ -- | Destination+ Path b1 Dir ->+ m ()+copyDirRecurGen preserveDirPermissions src dest = liftIO $ do+ bsrc <- makeAbsolute src+ bdest <- makeAbsolute dest+ (dirs, files) <- listDirRecur bsrc+ let swapParent ::+ Path Abs Dir ->+ Path Abs Dir ->+ Path Abs t ->+ IO (Path Abs t)+ swapParent old new path =+ (new </>)+ <$> stripProperPrefix old path+ ensureDir bdest+ copyPermissionsIOs <- forM dirs $ \srcDir -> do+ destDir <- swapParent bsrc bdest srcDir+ dirIsSymlink <- isSymlink srcDir+ if dirIsSymlink+ then do+ target <- getSymlinkTarget srcDir+ D.createDirectoryLink target $+ toFilePath' destDir+ else ensureDir destDir+ pure $ ignoringIOErrors (copyPermissions srcDir destDir)+ forM_ files $ \srcFile -> do+ destFile <- swapParent bsrc bdest srcFile+ fileIsSymlink <- isSymlink srcFile+ if fileIsSymlink+ then do+ target <- getSymlinkTarget srcFile+ D.createFileLink target (toFilePath destFile)+ else copyFile srcFile destFile+ when preserveDirPermissions $ do+ ignoringIOErrors (copyPermissions bsrc bdest)+ sequence_ copyPermissionsIOs++----------------------------------------------------------------------------+-- Walking directory trees++-- Recursive directory walk functionality, with a flexible API and avoidance+-- of loops. Following are some notes on the design.+--+-- Callback handler API:+--+-- The callback handler interface is designed to be highly flexible. There are+-- two possible alternative ways to control the traversal:+--+-- - In the context of the parent dir, decide which subdirs to descend into.+-- - In the context of the subdir, decide whether to traverse the subdir or not.+--+-- We choose the first approach here since it is more flexible and can+-- achieve everything that the second one can. The additional benefit with+-- this is that we can use the parent dir context efficiently instead of+-- each child looking at the parent context independently.+--+-- To control which subdirs to descend we use a 'WalkExclude' API instead of+-- a “WalkInclude” type of API so that the handlers cannot accidentally ask+-- us to descend a dir which is not a subdir of the directory being walked.+--+-- Avoiding Traversal Loops:+--+-- There can be loops in the path being traversed due to subdirectory+-- symlinks or filesystem corruptions can cause loops by creating directory+-- hardlinks. Also, if the filesystem is changing while we are traversing+-- then we might be going in loops due to the changes.+--+-- We record the path we are coming from to detect the loops. If we end up+-- traversing the same directory again we are in a loop.++-- | Action returned by the traversal handler function. The action controls+-- how the traversal will proceed.+--+-- __Note__: in version /1.4.0/ the type was adjusted to have the @b@ type+-- parameter.+--+-- @since 1.2.0+data WalkAction b+ = -- | Finish the entire walk altogether+ WalkFinish+ | -- | List of sub-directories to exclude from+ -- descending+ WalkExclude [Path b Dir]+ deriving (Eq, Show)++-- | Traverse a directory tree using depth first pre-order traversal,+-- calling a handler function at each directory node traversed. The absolute+-- paths of the parent directory, sub-directories and the files in the+-- directory are provided as arguments to the handler.+--+-- The function is capable of detecting and avoiding traversal loops in the+-- directory tree. Note that the traversal follows symlinks by default, an+-- appropriate traversal handler can be used to avoid that when necessary.+--+-- @since 1.2.0+walkDir ::+ (MonadIO m) =>+ -- | Handler (@dir -> subdirs -> files -> 'WalkAction'@)+ (Path Abs Dir -> [Path Abs Dir] -> [Path Abs File] -> m (WalkAction Abs)) ->+ -- | Directory where traversal begins+ Path b Dir ->+ m ()+walkDir handler topdir =+ void $+ makeAbsolute topdir >>= walkAvoidLoop S.empty+ where+ walkAvoidLoop traversed curdir = do+ mRes <- checkLoop traversed curdir+ case mRes of+ Nothing -> return $ Just ()+ Just traversed' -> walktree traversed' curdir+ walktree traversed curdir = do+ (subdirs, files) <- listDir curdir+ action <- handler curdir subdirs files+ case action of+ WalkFinish -> return Nothing+ WalkExclude xdirs ->+ case subdirs \\ xdirs of+ [] -> return $ Just ()+ ds ->+ runMaybeT $+ mapM_+ (MaybeT . walkAvoidLoop traversed)+ ds+ checkLoop traversed dir = do+ st <- liftIO $ P.getFileStatus (fromAbsDir dir)+ let ufid = (P.deviceID st, P.fileID st)+ return $+ if S.member ufid traversed+ then Nothing+ else Just (S.insert ufid traversed)++-- | The same as 'walkDir' but uses relative paths. The handler is given+-- @dir@, directory relative to the directory where traversal begins.+-- Sub-directories and files are relative to @dir@.+--+-- @since 1.4.2+walkDirRel ::+ (MonadIO m) =>+ -- | Handler (@dir -> subdirs -> files -> 'WalkAction'@)+ ( Path Rel Dir ->+ [Path Rel Dir] ->+ [Path Rel File] ->+ m (WalkAction Rel)+ ) ->+ -- | Directory where traversal begins+ Path b Dir ->+ m ()+walkDirRel handler topdir' = do+ topdir <- makeAbsolute topdir'+ let walkAvoidLoop traversed curdir = do+ mRes <- checkLoop traversed (topdir </> curdir)+ case mRes of+ Nothing -> return $ Just ()+ Just traversed' -> walktree traversed' curdir+ walktree traversed curdir = do+ (subdirs, files) <- listDirRel (topdir </> curdir)+ action <- handler curdir subdirs files+ case action of+ WalkFinish -> return Nothing+ WalkExclude xdirs ->+ case subdirs \\ xdirs of+ [] -> return $ Just ()+ ds ->+ runMaybeT $+ mapM_+ (MaybeT . walkAvoidLoop traversed)+ ((curdir </>) <$> ds)+ checkLoop traversed dir = do+ st <- liftIO $ P.getFileStatus (fromAbsDir dir)+ let ufid = (P.deviceID st, P.fileID st)+ return $+ if S.member ufid traversed+ then Nothing+ else Just (S.insert ufid traversed)+ void (walkAvoidLoop S.empty $(mkRelDir "."))++-- | Similar to 'walkDir' but accepts a 'Monoid'-returning output writer as+-- well. Values returned by the output writer invocations are accumulated+-- and returned.+--+-- Both, the descend handler as well as the output writer can be used for+-- side effects but keep in mind that the output writer runs before the+-- descend handler.+--+-- @since 1.2.0+walkDirAccum ::+ (MonadIO m, Monoid o) =>+ -- | Descend handler (@dir -> subdirs -> files -> 'WalkAction'@),+ -- descend the whole tree if omitted+ Maybe+ (Path Abs Dir -> [Path Abs Dir] -> [Path Abs File] -> m (WalkAction Abs)) ->+ -- | Output writer (@dir -> subdirs -> files -> o@)+ (Path Abs Dir -> [Path Abs Dir] -> [Path Abs File] -> m o) ->+ -- | Directory where traversal begins+ Path b Dir ->+ -- | Accumulation of outputs generated by the output writer invocations+ m o+walkDirAccum = walkDirAccumWith walkDir++-- | The same as 'walkDirAccum' but uses relative paths. The handler and+-- writer are given @dir@, directory relative to the directory where+-- traversal begins. Sub-directories and files are relative to @dir@.+--+-- @since 1.4.2+walkDirAccumRel ::+ (MonadIO m, Monoid o) =>+ -- | Descend handler (@dir -> subdirs -> files -> 'WalkAction'@),+ -- descend the whole tree if omitted+ Maybe+ (Path Rel Dir -> [Path Rel Dir] -> [Path Rel File] -> m (WalkAction Rel)) ->+ -- | Output writer (@dir -> subdirs -> files -> o@)+ (Path Rel Dir -> [Path Rel Dir] -> [Path Rel File] -> m o) ->+ -- | Directory where traversal begins+ Path b Dir ->+ -- | Accumulation of outputs generated by the output writer invocations+ m o+walkDirAccumRel = walkDirAccumWith walkDirRel++-- | Non-public helper function for defining accumulating walking actions.+walkDirAccumWith ::+ (MonadIO m, Monoid o) =>+ -- | The walk function we use+ ( ( Path a Dir ->+ [Path a Dir] ->+ [Path a File] ->+ WriterT o m (WalkAction a)+ ) ->+ Path b Dir ->+ WriterT o m ()+ ) ->+ -- | Descend handler (@dir -> subdirs -> files -> 'WalkAction'@),+ -- descend the whole tree if omitted+ Maybe (Path a Dir -> [Path a Dir] -> [Path a File] -> m (WalkAction a)) ->+ -- | Output writer (@dir -> subdirs -> files -> o@)+ (Path a Dir -> [Path a Dir] -> [Path a File] -> m o) ->+ -- | Directory where traversal begins+ Path b Dir ->+ -- | Accumulation of outputs generated by the output writer invocations+ m o+walkDirAccumWith walkF dHandler writer topdir =+ execWriterT (walkF handler topdir)+ where+ handler dir subdirs files = do+ res <- lift $ writer dir subdirs files+ tell res+ case dHandler of+ Just h -> lift $ h dir subdirs files+ Nothing -> return (WalkExclude [])++----------------------------------------------------------------------------+-- Current working directory++-- | Obtain the current working directory as an absolute path.+--+-- In a multithreaded program, the current working directory is a global+-- state shared among all threads of the process. Therefore, when performing+-- filesystem operations from multiple threads, it is highly recommended to+-- use absolute rather than relative paths (see: 'makeAbsolute').+--+-- The operation may fail with:+--+-- * 'HardwareFault'+-- A physical I\/O error has occurred.+-- @[EIO]@+--+-- * 'isDoesNotExistError' or 'NoSuchThing'+-- There is no path referring to the working directory.+-- @[EPERM, ENOENT, ESTALE...]@+--+-- * 'isPermissionError' or 'PermissionDenied'+-- The process has insufficient privileges to perform the operation.+-- @[EACCES]@+--+-- * 'ResourceExhausted'+-- Insufficient resources are available to perform the operation.+--+-- * 'UnsupportedOperation'+-- The operating system has no notion of current working directory.+getCurrentDir :: (MonadIO m) => m (Path Abs Dir)+getCurrentDir = liftIO $ D.getCurrentDirectory >>= parseAbsDir++-- | Change the working directory to the given path.+--+-- In a multithreaded program, the current working directory is a global+-- state shared among all threads of the process. Therefore, when performing+-- filesystem operations from multiple threads, it is highly recommended to+-- use absolute rather than relative paths (see: 'makeAbsolute').+--+-- The operation may fail with:+--+-- * 'HardwareFault'+-- A physical I\/O error has occurred.+-- @[EIO]@+--+-- * 'InvalidArgument'+-- The operand is not a valid directory name.+-- @[ENAMETOOLONG, ELOOP]@+--+-- * 'isDoesNotExistError' or 'NoSuchThing'+-- The directory does not exist.+-- @[ENOENT, ENOTDIR]@+--+-- * 'isPermissionError' or 'PermissionDenied'+-- The process has insufficient privileges to perform the operation.+-- @[EACCES]@+--+-- * 'UnsupportedOperation'+-- The operating system has no notion of current working directory, or the+-- working directory cannot be dynamically changed.+--+-- * 'InappropriateType'+-- The path refers to an existing non-directory object.+-- @[ENOTDIR]@+setCurrentDir :: (MonadIO m) => Path b Dir -> m ()+setCurrentDir = liftD D.setCurrentDirectory++-- | Run an 'IO' action with the given working directory and restore the+-- original working directory afterwards, even if the given action fails due+-- to an exception.+--+-- The operation may fail with the same exceptions as 'getCurrentDir' and+-- 'setCurrentDir'.+withCurrentDir ::+ (MonadIO m, MonadMask m) =>+ -- | Directory to execute in+ Path b Dir ->+ -- | Action to be executed+ m a ->+ m a+withCurrentDir dir action =+ bracket getCurrentDir setCurrentDir $ const (setCurrentDir dir >> action)++----------------------------------------------------------------------------+-- Pre-defined directories++-- | Return the current user's home directory.+--+-- The directory returned is expected to be writable by the current user,+-- but note that it isn't generally considered good practice to store+-- application-specific data here; use 'getAppUserDataDir' instead.+--+-- On Unix, 'getHomeDir' returns the value of the @HOME@ environment+-- variable. On Windows, the system is queried for a suitable path; a+-- typical path might be @C:\/Users\//\<user\>/@.+--+-- The operation may fail with:+--+-- * 'UnsupportedOperation'+-- The operating system has no notion of home directory.+--+-- * 'isDoesNotExistError'+-- The home directory for the current user does not exist, or+-- cannot be found.+getHomeDir :: (MonadIO m) => m (Path Abs Dir)+getHomeDir = liftIO D.getHomeDirectory >>= resolveDir'++-- | Obtain the path to a special directory for storing user-specific+-- application data (traditional Unix location).+--+-- The argument is usually the name of the application. Since it will be+-- integrated into the path, it must consist of valid path characters.+--+-- * On Unix-like systems, the path is @~\/./\<app\>/@.+-- * On Windows, the path is @%APPDATA%\//\<app\>/@+-- (e.g. @C:\/Users\//\<user\>/\/AppData\/Roaming\//\<app\>/@)+--+-- Note: the directory may not actually exist, in which case you would need+-- to create it. It is expected that the parent directory exists and is+-- writable.+--+-- The operation may fail with:+--+-- * 'UnsupportedOperation'+-- The operating system has no notion of application-specific data+-- directory.+--+-- * 'isDoesNotExistError'+-- The home directory for the current user does not exist, or cannot be+-- found.+getAppUserDataDir ::+ (MonadIO m) =>+ -- | Name of application (used in path construction)+ String ->+ m (Path Abs Dir)+getAppUserDataDir = liftIO . (>>= parseAbsDir) . D.getAppUserDataDirectory++-- | Return the current user's document directory.+--+-- The directory returned is expected to be writable by the current user,+-- but note that it isn't generally considered good practice to store+-- application-specific data here; use 'getAppUserDataDir' instead.+--+-- On Unix, 'getUserDocsDir' returns the value of the @HOME@ environment+-- variable. On Windows, the system is queried for a suitable path; a+-- typical path might be @C:\/Users\//\<user\>/\/Documents@.+--+-- The operation may fail with:+--+-- * 'UnsupportedOperation'+-- The operating system has no notion of document directory.+--+-- * 'isDoesNotExistError'+-- The document directory for the current user does not exist, or+-- cannot be found.+getUserDocsDir :: (MonadIO m) => m (Path Abs Dir)+getUserDocsDir = liftIO $ D.getUserDocumentsDirectory >>= parseAbsDir++-- | Return the current directory for temporary files.+--+-- On Unix, 'getTempDir' returns the value of the @TMPDIR@ environment+-- variable or \"\/tmp\" if the variable isn\'t defined. On Windows, the+-- function checks for the existence of environment variables in the+-- following order and uses the first path found:+--+-- *+-- TMP environment variable.+--+-- *+-- TEMP environment variable.+--+-- *+-- USERPROFILE environment variable.+--+-- *+-- The Windows directory+--+-- The operation may fail with:+--+-- * 'UnsupportedOperation'+-- The operating system has no notion of temporary directory.+--+-- The function doesn't verify whether the path exists.+getTempDir :: (MonadIO m) => m (Path Abs Dir)+getTempDir = liftIO D.getTemporaryDirectory >>= resolveDir'++-- | Obtain the paths to special directories for storing user-specific+-- application data, configuration, and cache files, conforming to the+-- <http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html XDG Base Directory Specification>.+-- Compared with 'getAppUserDataDir', this function provides a more+-- fine-grained hierarchy as well as greater flexibility for the user.+--+-- It also works on Windows, although in that case 'D.XdgData' and+-- 'D.XdgConfig' will map to the same directory.+--+-- Note: The directory may not actually exist, in which case you would need+-- to create it with file mode @700@ (i.e. only accessible by the owner).+--+-- Note also: this is a piece of conditional API, only available if+-- @directory-1.2.3.0@ or later is used.+--+-- @since 1.2.1+getXdgDir ::+ (MonadIO m) =>+ -- | Which special directory+ D.XdgDirectory ->+ -- | A relative path that is appended to the path; if 'Nothing', the+ -- base path is returned+ Maybe (Path Rel Dir) ->+ m (Path Abs Dir)+getXdgDir xdgDir suffix =+ liftIO $ (D.getXdgDirectory xdgDir $ maybe "" toFilePath suffix) >>= parseAbsDir++-- | Similar to 'getXdgDir' but retrieves the entire list of XDG+-- directories.+--+-- On Windows, 'D.XdgDataDirs' and 'D.XdgConfigDirs' usually map to the same+-- list of directories unless overridden.+--+-- Refer to the docs of 'D.XdgDirectoryList' for more details.+--+-- @since 1.5.0+getXdgDirList ::+ (MonadIO m) =>+ -- | Which special directory list+ D.XdgDirectoryList ->+ m [Path Abs Dir]+getXdgDirList xdgDirList =+ liftIO (D.getXdgDirectoryList xdgDirList >>= mapM parseAbsDir)++----------------------------------------------------------------------------+-- Path transformation++-- | Class of things ('Path's) that can be canonicalized, made absolute, and+-- made relative to a some base directory.+class AnyPath path where+ -- | Type of absolute version of the given @path@.+ type AbsPath path :: Type++ -- | Type of relative version of the given @path@.+ type RelPath path :: Type++ -- | Make a path absolute and remove as many indirections from it as+ -- possible. Indirections include the two special directories @.@ and+ -- @..@, as well as any symbolic links. The input path need not point to+ -- an existing file or directory.+ --+ -- __Note__: if you require only an absolute path, use 'makeAbsolute'+ -- instead. Most programs need not care about whether a path contains+ -- symbolic links.+ --+ -- Due to the fact that symbolic links are dependent on the state of the+ -- existing filesystem, the function can only make a conservative,+ -- best-effort attempt. Nevertheless, if the input path points to an+ -- existing file or directory, then the output path shall also point to+ -- the same file or directory.+ --+ -- Formally, symbolic links are removed from the longest prefix of the+ -- path that still points to an existing file. The function is not atomic,+ -- therefore concurrent changes in the filesystem may lead to incorrect+ -- results.+ --+ -- (Despite the name, the function does not guarantee canonicity of the+ -- returned path due to the presence of hard links, mount points, etc.)+ --+ -- /Known bug(s)/: on Windows, the function does not resolve symbolic+ -- links.+ --+ -- Please note that before version 1.2.3.0 of the @directory@ package,+ -- this function had unpredictable behavior on non-existent paths.+ canonicalizePath ::+ (MonadIO m) =>+ path ->+ m (AbsPath path)++ -- | Make a path absolute by prepending the current directory (if it isn't+ -- already absolute) and applying 'F.normalise' to the result.+ --+ -- If the path is already absolute, the operation never fails. Otherwise,+ -- the operation may fail with the same exceptions as 'getCurrentDir'.+ makeAbsolute ::+ (MonadIO m) =>+ path ->+ m (AbsPath path)++ -- | Make a path relative to a given directory.+ --+ -- @since 0.3.0+ makeRelative ::+ (MonadThrow m) =>+ -- | Base directory+ Path Abs Dir ->+ -- | Path that will be made relative to base directory+ path ->+ m (RelPath path)++ -- | Make a path relative to current working directory.+ --+ -- @since 0.3.0+ makeRelativeToCurrentDir ::+ (MonadIO m) =>+ path ->+ m (RelPath path)++instance AnyPath (Path b File) where+ type AbsPath (Path b File) = Path Abs File+ type RelPath (Path b File) = Path Rel File++ canonicalizePath = liftD $ D.canonicalizePath >=> parseAbsFile+ makeAbsolute = liftD $ D.makeAbsolute >=> parseAbsFile+ makeRelative b p = parseRelFile (F.makeRelative (toFilePath b) (toFilePath p))+ makeRelativeToCurrentDir p = liftIO $ getCurrentDir >>= flip makeRelative p++instance AnyPath (Path b Dir) where+ type AbsPath (Path b Dir) = Path Abs Dir+ type RelPath (Path b Dir) = Path Rel Dir++ canonicalizePath = liftD D.canonicalizePath >=> liftIO . parseAbsDir+ makeAbsolute = liftD D.makeAbsolute >=> liftIO . parseAbsDir+ makeRelative b p = parseRelDir (F.makeRelative (toFilePath b) (toFilePath p))+ makeRelativeToCurrentDir p = liftIO $ getCurrentDir >>= flip makeRelative p++-- | @since 1.8.0+instance AnyPath (SomeBase File) where+ type AbsPath (SomeBase File) = Path Abs File+ type RelPath (SomeBase File) = Path Rel File++ canonicalizePath s = case s of+ Abs a -> canonicalizePath a+ Rel a -> canonicalizePath a++ makeAbsolute s = case s of+ Abs a -> makeAbsolute a+ Rel a -> makeAbsolute a++ makeRelative r s = case s of+ Abs a -> makeRelative r a+ Rel a -> makeRelative r a++ makeRelativeToCurrentDir s = case s of+ Abs a -> makeRelativeToCurrentDir a+ Rel a -> makeRelativeToCurrentDir a++-- | @since 1.8.0+instance AnyPath (SomeBase Dir) where+ type AbsPath (SomeBase Dir) = Path Abs Dir+ type RelPath (SomeBase Dir) = Path Rel Dir++ canonicalizePath s = case s of+ Abs a -> canonicalizePath a+ Rel a -> canonicalizePath a++ makeAbsolute s = case s of+ Abs a -> makeAbsolute a+ Rel a -> makeAbsolute a++ makeRelative r s = case s of+ Abs a -> makeRelative r a+ Rel a -> makeRelative r a++ makeRelativeToCurrentDir s = case s of+ Abs a -> makeRelativeToCurrentDir a+ Rel a -> makeRelativeToCurrentDir a++-- | Append stringly-typed path to an absolute path and then canonicalize+-- it.+--+-- @since 0.3.0+resolveFile ::+ (MonadIO m) =>+ -- | Base directory+ Path Abs Dir ->+ -- | Path to resolve+ FilePath ->+ m (Path Abs File)+resolveFile b p = liftIO $ D.canonicalizePath (toFilePath b F.</> p) >>= parseAbsFile++-- | The same as 'resolveFile', but uses current working directory.+--+-- @since 0.3.0+resolveFile' ::+ (MonadIO m) =>+ -- | Path to resolve+ FilePath ->+ m (Path Abs File)+resolveFile' p = getCurrentDir >>= flip resolveFile p++-- | The same as 'resolveFile', but for directories.+--+-- @since 0.3.0+resolveDir ::+ (MonadIO m) =>+ -- | Base directory+ Path Abs Dir ->+ -- | Path to resolve+ FilePath ->+ m (Path Abs Dir)+resolveDir b p = liftIO $ D.canonicalizePath (toFilePath b F.</> p) >>= parseAbsDir++-- | The same as 'resolveDir', but uses current working directory.+--+-- @since 0.3.0+resolveDir' ::+ (MonadIO m) =>+ -- | Path to resolve+ FilePath ->+ m (Path Abs Dir)+resolveDir' p = getCurrentDir >>= flip resolveDir p++----------------------------------------------------------------------------+-- Actions on files++-- | @'removeFile' file@ removes the directory entry for an existing file+-- @file@, where @file@ is not itself a directory. The implementation may+-- specify additional constraints which must be satisfied before a file can+-- be removed (e.g. the file may not be in use by other processes).+--+-- The operation may fail with:+--+-- * 'HardwareFault'+-- A physical I\/O error has occurred.+-- @[EIO]@+--+-- * 'InvalidArgument'+-- The operand is not a valid file name.+-- @[ENAMETOOLONG, ELOOP]@+--+-- * 'isDoesNotExistError' \/ 'NoSuchThing'+-- The file does not exist.+-- @[ENOENT, ENOTDIR]@+--+-- * 'isPermissionError' \/ 'PermissionDenied'+-- The process has insufficient privileges to perform the operation.+-- @[EROFS, EACCES, EPERM]@+--+-- * 'UnsatisfiedConstraints'+-- Implementation-dependent constraints are not satisfied.+-- @[EBUSY]@+--+-- * 'InappropriateType'+-- The operand refers to an existing directory.+-- @[EPERM, EINVAL]@+removeFile :: (MonadIO m) => Path b File -> m ()+removeFile = liftD D.removeFile++-- | @'renameFile' old new@ changes the name of an existing file system+-- object from /old/ to /new/. If the /new/ object already exists, it is+-- atomically replaced by the /old/ object. Neither path may refer to an+-- existing directory. A conformant implementation need not support renaming+-- files in all situations (e.g. renaming across different physical+-- devices), but the constraints must be documented.+--+-- The operation may fail with:+--+-- * 'HardwareFault'+-- A physical I\/O error has occurred.+-- @[EIO]@+--+-- * 'InvalidArgument'+-- Either operand is not a valid file name.+-- @[ENAMETOOLONG, ELOOP]@+--+-- * 'isDoesNotExistError' \/ 'NoSuchThing'+-- The original file does not exist, or there is no path to the target.+-- @[ENOENT, ENOTDIR]@+--+-- * 'isPermissionError' \/ 'PermissionDenied'+-- The process has insufficient privileges to perform the operation.+-- @[EROFS, EACCES, EPERM]@+--+-- * 'ResourceExhausted'+-- Insufficient resources are available to perform the operation.+-- @[EDQUOT, ENOSPC, ENOMEM, EMLINK]@+--+-- * 'UnsatisfiedConstraints'+-- Implementation-dependent constraints are not satisfied.+-- @[EBUSY]@+--+-- * 'UnsupportedOperation'+-- The implementation does not support renaming in this situation.+-- @[EXDEV]@+--+-- * 'InappropriateType'+-- Either path refers to an existing directory.+-- @[ENOTDIR, EISDIR, EINVAL, EEXIST, ENOTEMPTY]@+renameFile ::+ (MonadIO m) =>+ -- | Original location+ Path b0 File ->+ -- | New location+ Path b1 File ->+ m ()+renameFile = liftD2 D.renameFile++-- | @'copyFile' old new@ copies the existing file from @old@ to @new@. If+-- the @new@ file already exists, it is atomically replaced by the @old@+-- file. Neither path may refer to an existing directory. The permissions of+-- @old@ are copied to @new@, if possible.+copyFile ::+ (MonadIO m) =>+ -- | Original location+ Path b0 File ->+ -- | Where to put copy+ Path b1 File ->+ m ()+copyFile = liftD2 D.copyFile++-- | Obtain the size of a file in bytes.+--+-- @since 1.7.0+getFileSize :: (MonadIO m) => Path b File -> m Integer+getFileSize = liftD D.getFileSize++-- | Given an executable file name, search for such file in the directories+-- listed in system @PATH@. The returned value is the path to the found+-- executable or 'Nothing' if an executable with the given name was not+-- found. For example ('findExecutable' \"ghc\") gives you the path to GHC.+--+-- The path returned by 'findExecutable' corresponds to the program that+-- would be executed by 'System.Process.createProcess' when passed the same+-- string (as a RawCommand, not a ShellCommand).+--+-- On Windows, 'findExecutable' calls the Win32 function 'SearchPath', which+-- may search other places before checking the directories in @PATH@. Where+-- it actually searches depends on registry settings, but notably includes+-- the directory containing the current executable. See+-- <http://msdn.microsoft.com/en-us/library/aa365527.aspx> for more details.+findExecutable ::+ (MonadIO m) =>+ -- | Executable file name+ Path Rel File ->+ -- | Path to found executable+ m (Maybe (Path Abs File))+findExecutable = fmap (>>= parseAbsFile) . liftD D.findExecutable++-- | Search through the given set of directories for the given file.+findFile ::+ (MonadIO m) =>+ -- | Set of directories to search in+ [Path b Dir] ->+ -- | Filename of interest+ Path Rel File ->+ -- | Absolute path to file (if found)+ m (Maybe (Path Abs File))+findFile [] _ = return Nothing+findFile (d : ds) file = do+ bfile <- (</> file) <$> makeAbsolute d+ exist <- doesFileExist bfile+ if exist+ then return (Just bfile)+ else findFile ds file++-- | Search through the given set of directories for the given file and+-- return a list of paths where the given file exists.+findFiles ::+ (MonadIO m) =>+ -- | Set of directories to search in+ [Path b Dir] ->+ -- | Filename of interest+ Path Rel File ->+ -- | Absolute paths to all found files+ m [Path Abs File]+findFiles = findFilesWith (const (return True))++-- | Search through the given set of directories for the given file and with+-- the given property (usually permissions) and return a list of paths where+-- the given file exists and has the property.+findFilesWith ::+ (MonadIO m) =>+ -- | How to test the files+ (Path Abs File -> m Bool) ->+ -- | Set of directories to search in+ [Path b Dir] ->+ -- | Filename of interest+ Path Rel File ->+ -- | Absolute paths to all found files+ m [Path Abs File]+findFilesWith _ [] _ = return []+findFilesWith f (d : ds) file = do+ bfile <- (</> file) <$> makeAbsolute d+ exist <- doesFileExist bfile+ b <- if exist then f bfile else return False+ if b+ then (bfile :) <$> findFilesWith f ds file+ else findFilesWith f ds file++----------------------------------------------------------------------------+-- Symbolic links++-- | Create a /file/ symbolic link. The target path can be either absolute+-- or relative and need not refer to an existing file. The order of+-- arguments follows the POSIX convention.+--+-- To remove an existing file symbolic link, use 'removeFile'.+--+-- Although the distinction between /file/ symbolic links and /directory/+-- symbolic links does not exist on POSIX systems, on Windows this is an+-- intrinsic property of every symbolic link and cannot be changed without+-- recreating the link. A file symbolic link that actually points to a+-- directory will fail to dereference and vice versa. Moreover, creating+-- symbolic links on Windows may require privileges unavailable to users+-- outside the Administrators group. Portable programs that use symbolic+-- links should take both into consideration.+--+-- On Windows, the function is implemented using @CreateSymbolicLink@. Since+-- 1.3.3.0, the @SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE@ flag is+-- included if supported by the operating system. On POSIX, the function+-- uses @symlink@ and is therefore atomic.+--+-- Windows-specific errors: This operation may fail with+-- 'System.IO.Error.permissionErrorType' if the user lacks the privileges to+-- create symbolic links. It may also fail with+-- 'System.IO.Error.illegalOperationErrorType' if the file system does not+-- support symbolic links.+--+-- @since 1.5.0+createFileLink ::+ (MonadIO m) =>+ -- | Path to the target file+ Path b0 File ->+ -- | Path to the link to be created+ Path b1 File ->+ m ()+createFileLink = liftD2 D.createFileLink++-- | Create a /directory/ symbolic link. The target path can be either+-- absolute or relative and need not refer to an existing directory. The+-- order of arguments follows the POSIX convention.+--+-- To remove an existing directory symbolic link, use 'removeDirLink'.+--+-- Although the distinction between /file/ symbolic links and /directory/+-- symbolic links does not exist on POSIX systems, on Windows this is an+-- intrinsic property of every symbolic link and cannot be changed without+-- recreating the link. A file symbolic link that actually points to a+-- directory will fail to dereference and vice versa. Moreover, creating+-- symbolic links on Windows may require privileges unavailable to users+-- outside the Administrators group. Portable programs that use symbolic+-- links should take both into consideration.+--+-- On Windows, the function is implemented using @CreateSymbolicLink@ with+-- @SYMBOLIC_LINK_FLAG_DIRECTORY@. Since 1.3.3.0, the+-- @SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE@ flag is also included if+-- supported by the operating system. On POSIX, this is an alias for+-- 'createFileLink' and is therefore atomic.+--+-- Windows-specific errors: This operation may fail with+-- 'System.IO.Error.permissionErrorType' if the user lacks the privileges to+-- create symbolic links. It may also fail with+-- 'System.IO.Error.illegalOperationErrorType' if the file system does not+-- support symbolic links.+--+-- @since 1.5.0+createDirLink ::+ (MonadIO m) =>+ -- | Path to the target directory+ Path b0 Dir ->+ -- | Path to the link to be created+ Path b1 Dir ->+ m ()+createDirLink target' dest' = do+ let target = toFilePath target'+ dest = toFilePath' dest'+ liftIO $ D.createDirectoryLink target dest++-- | Remove an existing /directory/ symbolic link.+--+-- On Windows, this is an alias for 'removeDir'. On POSIX systems, this is+-- an alias for 'removeFile'.+--+-- See also: 'removeFile', which can remove an existing /file/ symbolic link.+--+-- @since 1.5.0+removeDirLink ::+ (MonadIO m) =>+ -- | Path to the link to be removed+ Path b Dir ->+ m ()+removeDirLink = liftD D.removeDirectoryLink++-- | Retrieve the target path of either a file or directory symbolic link.+-- The returned path may not exist, and may not even be a valid path.+--+-- On Windows systems, this calls @DeviceIoControl@ with+-- @FSCTL_GET_REPARSE_POINT@. In addition to symbolic links, the function+-- also works on junction points. On POSIX systems, this calls @readlink@.+--+-- Windows-specific errors: This operation may fail with+-- 'System.IO.Error.illegalOperationErrorType' if the file system does not+-- support symbolic links.+--+-- @since 1.5.0+getSymlinkTarget ::+ (MonadIO m) =>+ -- | Symlink path+ Path b t ->+ m FilePath+getSymlinkTarget = liftD D.getSymbolicLinkTarget++-- | Check whether the path refers to a symbolic link. An exception is thrown+-- if the path does not exist or is inaccessible.+--+-- On Windows, this checks for @FILE_ATTRIBUTE_REPARSE_POINT@. In addition to+-- symbolic links, the function also returns true on junction points. On+-- POSIX systems, this checks for @S_IFLNK@.+--+-- @since 1.5.0++-- | Check if the given path is a symbolic link.+--+-- @since 1.3.0+isSymlink :: (MonadIO m) => Path b t -> m Bool+isSymlink = liftD D.pathIsSymbolicLink++----------------------------------------------------------------------------+-- Temporary files and directories++-- | Use a temporary file that doesn't already exist.+--+-- Creates a new temporary file inside the given directory, making use of+-- the template. The temporary file is deleted after use.+--+-- @since 0.2.0+withTempFile ::+ (MonadIO m, MonadMask m) =>+ -- | Directory to create the file in+ Path b Dir ->+ -- | File name template, see 'openTempFile'+ String ->+ -- | Callback that can use the file+ (Path Abs File -> Handle -> m a) ->+ m a+withTempFile path t action = do+ apath <- makeAbsolute path+ T.withTempFile (toFilePath apath) t $ \file h ->+ parseAbsFile file >>= flip action h++-- | Create and use a temporary directory.+--+-- Creates a new temporary directory inside the given directory, making use+-- of the template. The temporary directory is deleted after use.+--+-- @since 0.2.0+withTempDir ::+ (MonadIO m, MonadMask m) =>+ -- | Directory to create the file in+ Path b Dir ->+ -- | Directory name template, see 'openTempFile'+ String ->+ -- | Callback that can use the directory+ (Path Abs Dir -> m a) ->+ m a+withTempDir path t action = do+ apath <- makeAbsolute path+ T.withTempDirectory (toFilePath apath) t (parseAbsDir >=> action)++-- | Create and use a temporary file in the system standard temporary+-- directory.+--+-- Behaves exactly the same as 'withTempFile', except that the parent+-- temporary directory will be that returned by 'getTempDir'.+--+-- @since 0.2.0+withSystemTempFile ::+ (MonadIO m, MonadMask m) =>+ -- | File name template, see 'openTempFile'+ String ->+ -- | Callback that can use the file+ (Path Abs File -> Handle -> m a) ->+ m a+withSystemTempFile t action =+ getTempDir >>= \path ->+ withTempFile path t action++-- | Create and use a temporary directory in the system standard temporary+-- directory.+--+-- Behaves exactly the same as 'withTempDir', except that the parent+-- temporary directory will be that returned by 'getTempDir'.+--+-- @since 0.2.0+withSystemTempDir ::+ (MonadIO m, MonadMask m) =>+ -- | Directory name template, see 'openTempFile'+ String ->+ -- | Callback that can use the directory+ (Path Abs Dir -> m a) ->+ m a+withSystemTempDir t action =+ getTempDir >>= \path ->+ withTempDir path t action++-- | The function creates a temporary file in @rw@ mode. The created file+-- isn't deleted automatically, so you need to delete it manually.+--+-- The file is created with permissions such that only the current user can+-- read\/write it.+--+-- With some exceptions (see below), the file will be created securely in+-- the sense that an attacker should not be able to cause openTempFile to+-- overwrite another file on the filesystem using your credentials, by+-- putting symbolic links (on Unix) in the place where the temporary file is+-- to be created. On Unix the @O_CREAT@ and @O_EXCL@ flags are used to+-- prevent this attack, but note that @O_EXCL@ is sometimes not supported on+-- NFS filesystems, so if you rely on this behaviour it is best to use local+-- filesystems only.+--+-- @since 0.2.0+openTempFile ::+ (MonadIO m) =>+ -- | Directory to create file in+ Path b Dir ->+ -- | File name template; if the template is "foo.ext" then the created+ -- file will be @\"fooXXX.ext\"@ where @XXX@ is some random number+ String ->+ -- | Name of created file and its 'Handle'+ m (Path Abs File, Handle)+openTempFile path t = liftIO $ do+ apath <- makeAbsolute path+ (tfile, h) <- liftD2' T.openTempFile apath t+ (,h) <$> parseAbsFile tfile++-- | Like 'openTempFile', but opens the file in binary mode. On Windows,+-- reading a file in text mode (which is the default) will translate @CRLF@+-- to @LF@, and writing will translate @LF@ to @CRLF@. This is usually what+-- you want with text files. With binary files this is undesirable; also, as+-- usual under Microsoft operating systems, text mode treats control-Z as+-- EOF. Binary mode turns off all special treatment of end-of-line and+-- end-of-file characters.+--+-- @since 0.2.0+openBinaryTempFile ::+ (MonadIO m) =>+ -- | Directory to create file in+ Path b Dir ->+ -- | File name template, see 'openTempFile'+ String ->+ -- | Name of created file and its 'Handle'+ m (Path Abs File, Handle)+openBinaryTempFile path t = liftIO $ do+ apath <- makeAbsolute path+ (tfile, h) <- liftD2' T.openBinaryTempFile apath t+ (,h) <$> parseAbsFile tfile++-- | Create a temporary directory. The created directory isn't deleted+-- automatically, so you need to delete it manually.+--+-- The directory is created with permissions such that only the current user+-- can read\/write it.+--+-- @since 0.2.0+createTempDir ::+ (MonadIO m) =>+ -- | Directory to create file in+ Path b Dir ->+ -- | Directory name template, see 'openTempFile'+ String ->+ -- | Name of created temporary directory+ m (Path Abs Dir)+createTempDir path t =+ liftIO $+ makeAbsolute path >>= \apath ->+ liftD2' T.createTempDirectory apath t >>= parseAbsDir++----------------------------------------------------------------------------+-- Existence tests++-- | Test whether the given path points to an existing filesystem object. If+-- the user lacks necessary permissions to search the parent directories,+-- this function may return false even if the file does actually exist.+--+-- @since 1.7.0+doesPathExist :: (MonadIO m) => Path b t -> m Bool+doesPathExist = liftD D.doesPathExist++-- | The operation 'doesFileExist' returns 'True' if the argument file+-- exists and is not a directory, and 'False' otherwise.+doesFileExist :: (MonadIO m) => Path b File -> m Bool+doesFileExist = liftD D.doesFileExist++-- | The operation 'doesDirExist' returns 'True' if the argument file exists+-- and is either a directory or a symbolic link to a directory, and 'False'+-- otherwise.+doesDirExist :: (MonadIO m) => Path b Dir -> m Bool+doesDirExist = liftD D.doesDirectoryExist++-- | Check if there is a file or directory on specified path.+isLocationOccupied :: (MonadIO m) => Path b t -> m Bool+isLocationOccupied path = do+ let fp = toFilePath path+ file <- liftIO (D.doesFileExist fp)+ dir <- liftIO (D.doesDirectoryExist fp)+ return (file || dir)++-- | If argument of the function throws a+-- 'System.IO.Error.doesNotExistErrorType', 'Nothing' is returned (other+-- exceptions propagate). Otherwise the result is returned inside a 'Just'.+--+-- @since 0.3.0+forgivingAbsence :: (MonadIO m, MonadCatch m) => m a -> m (Maybe a)+forgivingAbsence f =+ catchIf+ isDoesNotExistError+ (Just <$> f)+ (const $ return Nothing)++-- | The same as 'forgivingAbsence', but ignores result.+--+-- @since 0.3.1+ignoringAbsence :: (MonadIO m, MonadCatch m) => m a -> m ()+ignoringAbsence = void . forgivingAbsence++----------------------------------------------------------------------------+-- Permissions++-- | The 'getPermissions' operation returns the permissions for the file or+-- directory.+--+-- The operation may fail with:+--+-- * 'isPermissionError' if the user is not permitted to access+-- the permissions; or+--+-- * 'isDoesNotExistError' if the file or directory does not exist.+getPermissions :: (MonadIO m) => Path b t -> m D.Permissions+getPermissions = liftD D.getPermissions++-- | The 'setPermissions' operation sets the permissions for the file or+-- directory.+--+-- The operation may fail with:+--+-- * 'isPermissionError' if the user is not permitted to set+-- the permissions; or+--+-- * 'isDoesNotExistError' if the file or directory does not exist.+setPermissions :: (MonadIO m) => Path b t -> D.Permissions -> m ()+setPermissions = liftD2' D.setPermissions++-- | Set permissions for the object found on second given path so they match+-- permissions of the object on the first path.+copyPermissions ::+ (MonadIO m) =>+ -- | From where to copy+ Path b0 t0 ->+ -- | What to modify+ Path b1 t1 ->+ m ()+copyPermissions = liftD2 D.copyPermissions++----------------------------------------------------------------------------+-- Timestamps++-- | Obtain the time at which the file or directory was last accessed.+--+-- The operation may fail with:+--+-- * 'isPermissionError' if the user is not permitted to read+-- the access time; or+--+-- * 'isDoesNotExistError' if the file or directory does not exist.+--+-- Caveat for POSIX systems: This function returns a timestamp with+-- sub-second resolution only if this package is compiled against+-- @unix-2.6.0.0@ or later and the underlying filesystem supports them.+--+-- Note: this is a piece of conditional API, only available if+-- @directory-1.2.3.0@ or later is used.+getAccessTime :: (MonadIO m) => Path b t -> m UTCTime+getAccessTime = liftD D.getAccessTime++-- | Change the time at which the file or directory was last accessed.+--+-- The operation may fail with:+--+-- * 'isPermissionError' if the user is not permitted to alter the+-- access time; or+--+-- * 'isDoesNotExistError' if the file or directory does not exist.+--+-- Some caveats for POSIX systems:+--+-- * Not all systems support @utimensat@, in which case the function can+-- only emulate the behavior by reading the modification time and then+-- setting both the access and modification times together. On systems+-- where @utimensat@ is supported, the access time is set atomically with+-- nanosecond precision.+--+-- * If compiled against a version of @unix@ prior to @2.7.0.0@, the+-- function would not be able to set timestamps with sub-second+-- resolution. In this case, there would also be loss of precision in the+-- modification time.+--+-- Note: this is a piece of conditional API, only available if+-- @directory-1.2.3.0@ or later is used.+setAccessTime :: (MonadIO m) => Path b t -> UTCTime -> m ()+setAccessTime = liftD2' D.setAccessTime++-- | Change the time at which the file or directory was last modified.+--+-- The operation may fail with:+--+-- * 'isPermissionError' if the user is not permitted to alter the+-- modification time; or+--+-- * 'isDoesNotExistError' if the file or directory does not exist.+--+-- Some caveats for POSIX systems:+--+-- * Not all systems support @utimensat@, in which case the function can+-- only emulate the behavior by reading the access time and then setting+-- both the access and modification times together. On systems where+-- @utimensat@ is supported, the modification time is set atomically with+-- nanosecond precision.+--+-- * If compiled against a version of @unix@ prior to @2.7.0.0@, the+-- function would not be able to set timestamps with sub-second+-- resolution. In this case, there would also be loss of precision in the+-- access time.+--+-- Note: this is a piece of conditional API, only available if+-- @directory-1.2.3.0@ or later is used.+setModificationTime :: (MonadIO m) => Path b t -> UTCTime -> m ()+setModificationTime = liftD2' D.setModificationTime++-- | Obtain the time at which the file or directory was last modified.+--+-- The operation may fail with:+--+-- * 'isPermissionError' if the user is not permitted to read+-- the modification time; or+--+-- * 'isDoesNotExistError' if the file or directory does not exist.+--+-- Caveat for POSIX systems: This function returns a timestamp with+-- sub-second resolution only if this package is compiled against+-- @unix-2.6.0.0@ or later and the underlying filesystem supports them.+getModificationTime :: (MonadIO m) => Path b t -> m UTCTime+getModificationTime = liftD D.getModificationTime++----------------------------------------------------------------------------+-- Helpers++-- | Lift an action in 'IO' that takes 'FilePath' into an action in slightly+-- more abstract monad that takes 'Path'.+liftD ::+ (MonadIO m) =>+ -- | Original action+ (FilePath -> IO a) ->+ -- | 'Path' argument+ Path b t ->+ -- | Lifted action+ m a+liftD m = liftIO . m . toFilePath'+{-# INLINE liftD #-}++-- | Similar to 'liftD' but for functions with arity 2.+liftD2 ::+ (MonadIO m) =>+ -- | Original action+ (FilePath -> FilePath -> IO a) ->+ -- | First 'Path' argument+ Path b0 t0 ->+ -- | Second 'Path' argument+ Path b1 t1 ->+ m a+liftD2 m a b = liftIO $ m (toFilePath' a) (toFilePath' b)+{-# INLINE liftD2 #-}++-- | Similar to 'liftD2', but allows us to pass second argument of arbitrary+-- type.+liftD2' ::+ (MonadIO m) =>+ -- | Original action+ (FilePath -> v -> IO a) ->+ -- | First 'Path' argument+ Path b t ->+ -- | Second argument+ v ->+ m a+liftD2' m a v = liftIO $ m (toFilePath' a) v+{-# INLINE liftD2' #-}++-- | Like 'toFilePath', but also drops the trailing path separator.+toFilePath' :: Path b t -> FilePath+toFilePath' = F.dropTrailingPathSeparator . toFilePath++-- | Perform an action ignoring IO exceptions it may throw.+ignoringIOErrors :: IO () -> IO ()+ignoringIOErrors ioe = ioe `catch` handler+ where+ handler :: (Monad m) => IOError -> m () handler = const (return ())
README.md view
@@ -4,20 +4,25 @@ [](https://hackage.haskell.org/package/path-io) [](http://stackage.org/nightly/package/path-io) [](http://stackage.org/lts/package/path-io)-[](https://travis-ci.org/mrkkrp/path-io)-[](https://ci.appveyor.com/project/mrkkrp/path-io/branch/master)-[](https://coveralls.io/github/mrkkrp/path-io?branch=master)+ -This package provides an interface to-the [`directory`](https://hackage.haskell.org/package/directory) package for-users of Chris Done's [`path`](https://hackage.haskell.org/package/path). It-also implements some missing stuff like recursive scanning and copying of+This package provides an interface to the+[`directory`](https://hackage.haskell.org/package/directory) package for+users of [`path`](https://hackage.haskell.org/package/path). It also+implements some extra functionality like recursive scanning and copying of directories, working with temporary files/directories, etc. Consult Haddocks for usage, which should be trivial. +## Contribution++Issues, bugs, and questions may be reported in [the GitHub issue tracker for+this project](https://github.com/mrkkrp/path-io/issues).++Pull requests are also welcome.+ ## License -Copyright © 2016–2017 Mark Karpov+Copyright © 2016–present Mark Karpov Distributed under BSD 3 clause license.
− Setup.hs
@@ -1,6 +0,0 @@-module Main (main) where--import Distribution.Simple--main :: IO ()-main = defaultMain
path-io.cabal view
@@ -1,62 +1,74 @@-name: path-io-version: 1.3.3-cabal-version: >= 1.18-tested-with: GHC==7.8.4, GHC==7.10.3, GHC==8.0.2, GHC==8.2.1-license: BSD3-license-file: LICENSE.md-author: Mark Karpov <markkarpov92@gmail.com>-maintainer: Mark Karpov <markkarpov92@gmail.com>-homepage: https://github.com/mrkkrp/path-io-bug-reports: https://github.com/mrkkrp/path-io/issues-category: System, Filesystem-synopsis: Interface to ‘directory’ package for users of ‘path’-build-type: Simple-description: Interface to ‘directory’ package for users of ‘path’.-extra-doc-files: CHANGELOG.md- , README.md+cabal-version: 2.4+name: path-io+version: 1.8.2+license: BSD-3-Clause+license-file: LICENSE.md+maintainer: Mark Karpov <markkarpov92@gmail.com>+author: Mark Karpov <markkarpov92@gmail.com>+tested-with: ghc ==9.6.5 ghc ==9.8.2 ghc ==9.10.1+homepage: https://github.com/mrkkrp/path-io+bug-reports: https://github.com/mrkkrp/path-io/issues+synopsis: Interface to ‘directory’ package for users of ‘path’+description: Interface to ‘directory’ package for users of ‘path’.+category: System, Filesystem+build-type: Simple+extra-doc-files:+ CHANGELOG.md+ README.md +source-repository head+ type: git+ location: https://github.com/mrkkrp/path-io.git+ flag dev- description: Turn on development settings.- manual: True- default: False+ description: Turn on development settings.+ default: False+ manual: True library- build-depends: base >= 4.7 && < 5.0- , containers- , directory >= 1.2.2 && < 1.4- , dlist >= 0.8 && < 0.9- , exceptions >= 0.8 && < 0.9- , filepath >= 1.2 && < 1.5- , path >= 0.5 && < 0.7- , temporary >= 1.1 && < 1.3- , time >= 1.4 && < 1.9- , transformers >= 0.3 && < 0.6- , unix-compat- exposed-modules: Path.IO- if flag(dev)- ghc-options: -Wall -Werror- else- ghc-options: -O2 -Wall- default-language: Haskell2010+ exposed-modules: Path.IO+ default-language: GHC2021+ build-depends:+ base >=4.15 && <5,+ containers,+ directory >=1.3.2.0 && <1.4,+ dlist >=0.8 && <2,+ exceptions >=0.8 && <0.11,+ filepath >=1.2 && <1.6,+ path >=0.7.1 && <0.10,+ temporary >=1.1 && <1.4,+ time >=1.4 && <1.15,+ transformers >=0.3 && <0.7,+ unix-compat + if flag(dev)+ ghc-options:+ -Wall -Werror -Wredundant-constraints -Wpartial-fields+ -Wunused-packages++ else+ ghc-options: -O2 -Wall+ test-suite tests- main-is: Main.hs- hs-source-dirs: tests- type: exitcode-stdio-1.0- if flag(dev)- ghc-options: -Wall -Werror- else- ghc-options: -O2 -Wall- build-depends: base >= 4.7 && < 5.0- , directory >= 1.2.2 && < 1.4- , exceptions >= 0.8 && < 0.9- , hspec >= 2.0 && < 3.0- , path >= 0.5 && < 0.7- , path-io- , transformers >= 0.3 && < 0.6- , unix-compat- default-language: Haskell2010+ type: exitcode-stdio-1.0+ main-is: Main.hs+ hs-source-dirs: tests+ default-language: GHC2021+ build-depends:+ base >=4.15 && <5,+ exceptions >=0.8 && <0.11,+ hspec >=2 && <3,+ path >=0.7.1 && <0.10,+ path-io,+ unix-compat -source-repository head- type: git- location: https://github.com/mrkkrp/path-io.git+ if flag(dev)+ ghc-options:+ -Wall -Werror -Wredundant-constraints -Wpartial-fields+ -Wunused-packages++ else+ ghc-options: -O2 -Wall++ if impl(ghc >=9.8)+ ghc-options: -Wno-x-partial
tests/Main.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE TemplateHaskell #-} module Main (main) where@@ -6,135 +6,142 @@ import Control.Monad import Control.Monad.Catch import Control.Monad.IO.Class (MonadIO (..))+import Data.Bifunctor import Data.List (sort) import Path import Path.IO-import Test.Hspec import System.Environment import System.PosixCompat.Files--#if !MIN_VERSION_base(4,8,0)-import Control.Applicative ((<$>))-#endif+import Test.Hspec main :: IO () main = hspec . around withSandbox $ do++{- ORMOLU_DISABLE -}+ #ifndef mingw32_HOST_OS beforeWith populatedDir $ do- -- NOTE These tests shall fail on Windows as unix-compat does not- -- implement createSymbolicLink for windows.+ -- NOTE These tests fail on Windows as unix-compat does not implement+ -- createSymbolicLink for windows. describe "listDir" listDirSpec+ describe "listDirRel" listDirRelSpec describe "listDirRecur" listDirRecurSpec+ describe "listDirRecurRel" listDirRecurRelSpec describe "listDirRecurWith" listDirRecurWithSpec describe "walkDir Finish" walkDirFinishSpec describe "copyDirRecur" copyDirRecurSpec describe "copyDirRecur'" copyDirRecur'Spec describe "findFile" findFileSpec+ describe "removeDirLink" removeDirLinkSpec beforeWith populatedCyclicDir $ describe "listDirRecur Cyclic" listDirRecurCyclicSpec #endif- describe "getCurrentDir" getCurrentDirSpec- describe "setCurrentDir" setCurrentDirSpec- describe "withCurrentDir" withCurrentDirSpec+ describe "getCurrentDir" getCurrentDirSpec+ describe "setCurrentDir" setCurrentDirSpec+ describe "withCurrentDir" withCurrentDirSpec+ describe "walkDirRel" walkDirRelSpec #ifndef mingw32_HOST_OS -- NOTE We can't quite test this on Windows as well, because the -- environmental variables HOME and TMPDIR do not exist there. describe "getHomeDir" getHomeDirSpec describe "getTempDir" getTempDirSpec-#if MIN_VERSION_directory(1,2,3) describe "getXdgDir Data" getXdgDataDirSpec describe "getXdgDir Config" getXdgConfigDirSpec describe "getXdgDir Cache" getXdgCacheDirSpec #endif-#endif +{- ORMOLU_ENABLE -}+ listDirSpec :: SpecWith (Path Abs Dir) listDirSpec = it "lists directory" $ \dir -> getDirStructure listDir dir `shouldReturn` populatedDirTop +listDirRelSpec :: SpecWith (Path Abs Dir)+listDirRelSpec = it "lists directory" $ \dir ->+ getDirStructureRel listDirRel dir `shouldReturn` populatedDirTop+ listDirRecurSpec :: SpecWith (Path Abs Dir) listDirRecurSpec = it "lists directory recursively" $ \dir -> getDirStructure listDirRecur dir `shouldReturn` populatedDirStructure +listDirRecurRelSpec :: SpecWith (Path Abs Dir)+listDirRecurRelSpec = it "lists directory recursively" $ \dir ->+ getDirStructureRel listDirRecurRel dir `shouldReturn` populatedDirStructure+ listDirRecurWithSpec :: SpecWith (Path Abs Dir) listDirRecurWithSpec = it "lists directory recursively using predicates" $ \dir ->- getDirStructure (listDirRecurWith- (return . ($(mkRelDir "c") /=) . dirname)- (return . ($(mkRelFile "two.txt") /=) . filename)) dir- `shouldReturn` populatedDirRecurWith+ getDirStructure+ ( listDirRecurWith+ (return . ($(mkRelDir "c") /=) . dirname)+ (return . ($(mkRelFile "two.txt") /=) . filename)+ )+ dir+ `shouldReturn` populatedDirRecurWith -listDirRecurWith- :: (Path Abs Dir -> IO Bool) -- ^ Dir match predicate- -> (Path Abs File -> IO Bool) -- ^ File match predicate- -> Path Abs Dir -- ^ Top dir to traverse- -> IO ([Path Abs Dir], [Path Abs File]) -- ^ Matched subdirs and files+listDirRecurWith ::+ -- | Dir match predicate+ (Path Abs Dir -> IO Bool) ->+ -- | File match predicate+ (Path Abs File -> IO Bool) ->+ -- | Top dir to traverse+ Path Abs Dir ->+ -- | Matched subdirs and files+ IO ([Path Abs Dir], [Path Abs File]) listDirRecurWith dirPred filePred = walkDirAccum Nothing $ \_ d f -> do- d' <- filterM dirPred d+ d' <- filterM dirPred d f' <- filterM filePred f return (d', f') --- Follows symbolic links-listDirRecurCyclic :: (MonadIO m, MonadThrow m)- => Path b Dir -- ^ Directory to list- -> m ([Path Abs Dir], [Path Abs File]) -- ^ Sub-directories and files-listDirRecurCyclic = walkDirAccum Nothing (\_ d f -> return (d, f))--listDirRecurCyclicSpec :: SpecWith (Path Abs Dir)-listDirRecurCyclicSpec =- it "lists directory trees having traversal cycles" $ \dir ->- getDirStructure listDirRecurCyclic dir- `shouldReturn` populatedCyclicDirStructure---- | walkDir with a Finish handler may have unpredictable output depending on--- the order of traversal. The only guarantee is that we will finish only after--- we find the directory "c". Though if we test only for the presence of "c" we--- are not really testing if we indeed cut the traversal short.-+-- | 'walkDir' with a 'WalkFinish' handler may have unpredictable output+-- depending on the order of traversal. The only guarantee is that we will+-- finish only after we find the directory "c". Though if we test only for+-- the presence of "c" we are not really testing if we indeed cut the+-- traversal short. walkDirFinishSpec :: SpecWith (Path Abs Dir) walkDirFinishSpec = it "Finishes only after finding what it is looking for" $ \dir -> do (d, _) <- getDirStructure (walkDirAccum (Just dHandler) writer) dir map dirname d `shouldContain` [$(mkRelDir "c")]- where dHandler p _ _- | dirname p == $(mkRelDir "c") = return WalkFinish- | otherwise = return (WalkExclude [])-- writer _ d f = return (d, f)+ where+ dHandler p _ _+ | dirname p == $(mkRelDir "c") = return WalkFinish+ | otherwise = return (WalkExclude [])+ writer _ d f = return (d, f) copyDirRecurSpec :: SpecWith (Path Abs Dir) copyDirRecurSpec = do context "when source directory is editable" $ it "copies directory" $ \src -> do- let dest = parent src </> $(mkRelDir "copied-dir")- copyDirRecur src dest- old <- getDirStructure listDirRecur src- new <- getDirStructure listDirRecur dest- old `shouldBe` new+ let dest = parent src </> $(mkRelDir "copied-dir")+ copyDirRecur src dest+ old <- getDirStructure listDirRecur src+ new <- getDirStructure listDirRecur dest+ old `shouldBe` new context "when source directory is read-only" $ it "copies directory just as well (preserving permissions)" $ \src -> do- let dest = parent src </> $(mkRelDir "copied-dir")- srcPermissions <- setOwnerWritable False <$> getPermissions src- setPermissions src srcPermissions- copyDirRecur src dest- old <- getDirStructure listDirRecur src- new <- getDirStructure listDirRecur dest- old `shouldBe` new- getPermissions dest `shouldReturn` srcPermissions+ let dest = parent src </> $(mkRelDir "copied-dir")+ srcPermissions <- setOwnerWritable False <$> getPermissions src+ setPermissions src srcPermissions+ copyDirRecur src dest+ old <- getDirStructure listDirRecur src+ new <- getDirStructure listDirRecur dest+ old `shouldBe` new+ getPermissions dest `shouldReturn` srcPermissions copyDirRecur'Spec :: SpecWith (Path Abs Dir) copyDirRecur'Spec = context "when source directory is read-only" $ it "copies directory but now it's editable" $ \src -> do- let dest = parent src </> $(mkRelDir "copied-dir")- srcPermissions <- setOwnerWritable False <$> getPermissions src- setPermissions src srcPermissions- copyDirRecur' src dest- old <- getDirStructure listDirRecur src- new <- getDirStructure listDirRecur dest- old `shouldBe` new- getPermissions dest `shouldReturn` srcPermissions { writable = True }+ let dest = parent src </> $(mkRelDir "copied-dir")+ srcPermissions <- setOwnerWritable False <$> getPermissions src+ setPermissions src srcPermissions+ copyDirRecur' src dest+ old <- getDirStructure listDirRecur src+ new <- getDirStructure listDirRecur dest+ old `shouldBe` new+ getPermissions dest `shouldReturn` srcPermissions {writable = True} findFileSpec :: SpecWith (Path Abs Dir) findFileSpec = it "finds a file lazily" $ \dir -> do@@ -142,6 +149,30 @@ found <- findFile (dir : undefined) relFile found `shouldBe` Just (dir </> relFile) +removeDirLinkSpec :: SpecWith (Path Abs Dir)+removeDirLinkSpec = it "remove dir link" $ \dir -> do+ let target = dir </> $(mkRelDir "a")+ link = dir </> $(mkRelDir "link-a")+ createDirLink target link+ removeDirLink link+ exists <- doesDirExist link+ exists `shouldBe` False++listDirRecurCyclicSpec :: SpecWith (Path Abs Dir)+listDirRecurCyclicSpec =+ it "lists directory trees having traversal cycles" $ \dir ->+ getDirStructure listDirRecurCyclic dir+ `shouldReturn` populatedCyclicDirStructure++-- | Follows symbolic links.+listDirRecurCyclic ::+ (MonadIO m) =>+ -- | Directory to list+ Path b Dir ->+ -- | Sub-directories and files+ m ([Path Abs Dir], [Path Abs File])+listDirRecurCyclic = walkDirAccum Nothing (\_ d f -> return (d, f))+ getCurrentDirSpec :: SpecWith (Path Abs Dir) getCurrentDirSpec = it "returns current dir" $ \dir -> getCurrentDir `shouldNotReturn` dir@@ -150,7 +181,7 @@ setCurrentDirSpec = it "sets current dir" $ \dir -> do wdir <- getCurrentDir setCurrentDir dir- new <- getCurrentDir+ new <- getCurrentDir setCurrentDir wdir new `shouldBe` dir @@ -160,24 +191,34 @@ getCurrentDir `shouldReturn` dir getCurrentDir `shouldNotReturn` dir +walkDirRelSpec :: SpecWith (Path Abs Dir)+walkDirRelSpec = it "does not throw exceptions" $ \dir -> do+ let handler curdir subdirs files = do+ curdir `shouldBe` $(mkRelDir ".")+ subdirs `shouldBe` []+ files `shouldBe` []+ return WalkFinish+ walkDirRel handler dir+ getHomeDirSpec :: SpecWith (Path Abs Dir) getHomeDirSpec = it "home dir is influenced by environment variable HOME" $ \dir -> bracket (getEnv evar) (setEnv evar) $ \_ -> do setEnv evar (toFilePath dir) getHomeDir `shouldReturn` dir- where evar = "HOME"+ where+ evar = "HOME" getTempDirSpec :: SpecWith (Path Abs Dir) getTempDirSpec = it "temp dir is influenced by environment variable TMPDIR" $ \dir ->- flip finally (unsetEnv evar) $ do+ flip finally (unsetEnv evar) $ do setEnv evar (toFilePath dir) getTempDir `shouldReturn` dir unsetEnv evar- where evar = "TMPDIR"+ where+ evar = "TMPDIR" -#if MIN_VERSION_directory(1,2,3) getXdgDataDirSpec :: SpecWith (Path Abs Dir) getXdgDataDirSpec = it "XDG data dir is influenced by environment variable XDG_DATA_HOME" $ \dir ->@@ -186,8 +227,9 @@ getXdgDir XdgData (Just name) `shouldReturn` (dir </> name) getXdgDir XdgData Nothing `shouldReturn` dir unsetEnv evar- where evar = "XDG_DATA_HOME"- name = $(mkRelDir "test")+ where+ evar = "XDG_DATA_HOME"+ name = $(mkRelDir "test") getXdgConfigDirSpec :: SpecWith (Path Abs Dir) getXdgConfigDirSpec =@@ -197,8 +239,9 @@ getXdgDir XdgConfig (Just name) `shouldReturn` (dir </> name) getXdgDir XdgConfig Nothing `shouldReturn` dir unsetEnv evar- where evar = "XDG_CONFIG_HOME"- name = $(mkRelDir "test")+ where+ evar = "XDG_CONFIG_HOME"+ name = $(mkRelDir "test") getXdgCacheDirSpec :: SpecWith (Path Abs Dir) getXdgCacheDirSpec =@@ -208,18 +251,16 @@ getXdgDir XdgCache (Just name) `shouldReturn` (dir </> name) getXdgDir XdgCache Nothing `shouldReturn` dir unsetEnv evar- where evar = "XDG_CACHE_HOME"- name = $(mkRelDir "test")-#endif+ where+ evar = "XDG_CACHE_HOME"+ name = $(mkRelDir "test") ---------------------------------------------------------------------------- -- Helpers -- | Create a sandbox directory to model some situation in it and run some--- tests. Note that we're using new unique sandbox directory for each test--- case to avoid contamination and it's unconditionally deleted after test--- case finishes.-+-- tests. Note that we're using a new unique sandbox directory for each test+-- case and it's unconditionally deleted after test case finishes. withSandbox :: ActionWith (Path Abs Dir) -> IO () withSandbox = withSystemTempDir "path-io-sandbox" @@ -227,48 +268,66 @@ -- to that directory. -- -- Created objects are described in 'populatedDirStructure'.- populatedDir :: Path Abs Dir -> IO (Path Abs Dir) populatedDir root = do let (_, files) = populatedDirStructure- pdir = root </> $(mkRelDir "pdir")+ pdir = root </> $(mkRelDir "pdir") withinSandbox = (pdir </>) ensureDir pdir- ensureDir $ withinSandbox $(mkRelDir "b")+ let b = withinSandbox $(mkRelDir "b")+ ensureDir b ensureDir $ withinSandbox $(mkRelDir "b/c")- -- to verify that we do not follow symbolic links. We should not list b's- -- tree under 'a'.+ -- Create a read-only directory with a file inside to test that the code+ -- that copies directory permissions can handle that gracefully.+ --+ -- See: https://github.com/mrkkrp/path-io/pull/82+ let readonlyDir = withinSandbox $(mkRelDir "readonly-dir")+ ensureDir readonlyDir+ -- We should not list b's tree under 'a' in order to verify that we do not+ -- follow symbolic links. createSymbolicLink "b" (toFilePath $ withinSandbox $(mkRelFile "a")) forM_ files $ (`writeFile` "") . toFilePath . withinSandbox+ getPermissions readonlyDir+ >>= setPermissions readonlyDir . setOwnerWritable False return pdir --- | Get inner structure of a directory. Items are sorted, so it's easier to--- compare results.--getDirStructure- :: (Path Abs Dir -> IO ([Path Abs Dir], [Path Abs File]))- -- ^ Which function to use for scanning- -> Path Abs Dir- -- ^ Path to directory to scan- -> IO ([Path Rel Dir], [Path Rel File])+-- | Get the inner structure of a directory. Items are sorted, so it's+-- easier to compare results.+getDirStructure ::+ -- | Which function to use for scanning+ (Path Abs Dir -> IO ([Path Abs Dir], [Path Abs File])) ->+ -- | Path to directory to scan+ Path Abs Dir ->+ IO ([Path Rel Dir], [Path Rel File]) getDirStructure f path = do (dirs, files) <- f path- rdirs <- sort <$> mapM (makeRelative path) dirs+ rdirs <- sort <$> mapM (makeRelative path) dirs rfiles <- sort <$> mapM (makeRelative path) files return (rdirs, rfiles) +-- | A version of 'getDirStructure' that accepts scanning function that+-- returns relative paths.+getDirStructureRel ::+ -- | Which function to use for scanning+ (Path Abs Dir -> IO ([Path Rel Dir], [Path Rel File])) ->+ -- | Path to directory to scan+ Path Abs Dir ->+ IO ([Path Rel Dir], [Path Rel File])+getDirStructureRel f path = bimap sort sort <$> f path+ -- | Structure of directory created by the 'populatedDir' function. Please -- keep it sorted.- populatedDirStructure :: ([Path Rel Dir], [Path Rel File]) populatedDirStructure =- ( [ $(mkRelDir "a")- , $(mkRelDir "b")- , $(mkRelDir "b/c")- ]- , [ $(mkRelFile "b/c/three.txt")- , $(mkRelFile "b/two.txt")- , $(mkRelFile "one.txt")+ ( [ $(mkRelDir "a"),+ $(mkRelDir "b"),+ $(mkRelDir "b/c"),+ $(mkRelDir "readonly-dir")+ ],+ [ $(mkRelFile "b/c/three.txt"),+ $(mkRelFile "b/two.txt"),+ $(mkRelFile "one.txt"),+ $(mkRelFile "readonly-dir/two.txt") ] ) @@ -281,29 +340,26 @@ -- c\/(d -> a), a\/(b -> c) -- 2) Cycle with own ancestor -- e\/f\/(g -> e)- populatedCyclicDirStructure :: ([Path Rel Dir], [Path Rel File]) populatedCyclicDirStructure =- ( [- $(mkRelDir "a")- , $(mkRelDir "a/b") -- b points to c- , $(mkRelDir "a/b/d") -- because b is same as c- , $(mkRelDir "c")- , $(mkRelDir "c/d") -- d points to a- , $(mkRelDir "c/d/b") -- because d is same as a- , $(mkRelDir "e")- , $(mkRelDir "e/f")- , $(mkRelDir "e/f/g") -- g points to e- ]- , []+ ( [ $(mkRelDir "a"),+ $(mkRelDir "a/b"), -- b points to c+ $(mkRelDir "a/b/d"), -- because b is same as c+ $(mkRelDir "c"),+ $(mkRelDir "c/d"), -- d points to a+ $(mkRelDir "c/d/b"), -- because d is same as a+ $(mkRelDir "e"),+ $(mkRelDir "e/f"),+ $(mkRelDir "e/f/g") -- g points to e+ ],+ [] ) -- | Created the objects described in 'populatedCyclicDirStructure'. -- Return path to that directory.- populatedCyclicDir :: Path Abs Dir -> IO (Path Abs Dir) populatedCyclicDir root = do- let pdir = root </> $(mkRelDir "pdir")+ let pdir = root </> $(mkRelDir "pdir") withinSandbox = (pdir </>) ensureDir pdir ensureDir $ withinSandbox $(mkRelDir "a")@@ -314,29 +370,29 @@ createSymbolicLink "../../e" (toFilePath $ withinSandbox $(mkRelFile "e/f/g")) return pdir --- | Top-level structure of populated directory as it should be scanned by--- the 'listDir' function.-+-- | Top-level structure of the populated directory as it should be scanned+-- by the 'listDir' function. populatedDirTop :: ([Path Rel Dir], [Path Rel File]) populatedDirTop =- ( [ $(mkRelDir "a")- , $(mkRelDir "b")- ]- , [ $(mkRelFile "one.txt")+ ( [ $(mkRelDir "a"),+ $(mkRelDir "b"),+ $(mkRelDir "readonly-dir")+ ],+ [ $(mkRelFile "one.txt") ] ) --- | Structure of populated directory as it should be scanned by--- 'listDirRecurWith' function using predicates to filter out dir 'c' and the--- file 'two.txt'-+-- | Structure of the populated directory as it should be scanned by+-- 'listDirRecurWith' function using predicates to filter out dir 'c' and+-- the file @two.txt@. populatedDirRecurWith :: ([Path Rel Dir], [Path Rel File]) populatedDirRecurWith =- ( [ $(mkRelDir "a")- , $(mkRelDir "b")- ]- , [ $(mkRelFile "a/c/three.txt") -- via symbolic link- , $(mkRelFile "b/c/three.txt")- , $(mkRelFile "one.txt")+ ( [ $(mkRelDir "a"),+ $(mkRelDir "b"),+ $(mkRelDir "readonly-dir")+ ],+ [ $(mkRelFile "a/c/three.txt"), -- via a symbolic link+ $(mkRelFile "b/c/three.txt"),+ $(mkRelFile "one.txt") ] )