Abstract over Path more properly

We don't expect "Path Abs" everywhere anymore. The functions
have been made to be more generic. A user can still pass
absolute paths, so we don't lose any safety. However, some
function implementations may be more tricky.
This commit is contained in:
Julian Ospald 2018-04-11 00:44:47 +02:00
parent 1c95c9f8f9
commit 3e6d93182a
No known key found for this signature in database
GPG Key ID: 511B62C09D50CD28
4 changed files with 191 additions and 139 deletions

View File

@ -8,8 +8,11 @@
-- Portability : portable -- Portability : portable
-- --
-- This module provides high-level IO related file operations like -- This module provides high-level IO related file operations like
-- copy, delete, move and so on. It only operates on /Path Abs/ which -- copy, delete, move and so on. It only operates on /Path x/ which
-- guarantees us well-typed paths which are absolute. -- guarantees us well-typed paths. Passing in /Path Abs/ to any
-- of these functions generally increases safety. Passing /Path Rel/
-- may trigger looking up the current directory via `getcwd` in some
-- cases where it cannot be avoided.
-- --
-- Some functions are just path-safe wrappers around -- Some functions are just path-safe wrappers around
-- unix functions, others have stricter exception handling -- unix functions, others have stricter exception handling
@ -75,6 +78,7 @@ module HPath.IO
, getFileType , getFileType
-- * Others -- * Others
, canonicalizePath , canonicalizePath
, toAbs
) )
where where
@ -184,6 +188,7 @@ import System.Posix.ByteString
import System.Posix.Directory.ByteString import System.Posix.Directory.ByteString
( (
createDirectory createDirectory
, getWorkingDirectory
, removeDirectory , removeDirectory
) )
import System.Posix.Directory.Traversals import System.Posix.Directory.Traversals
@ -326,9 +331,9 @@ data CopyMode = Strict -- ^ fail if any target exists
-- Throws in `Strict` CopyMode only: -- Throws in `Strict` CopyMode only:
-- --
-- - `AlreadyExists` if destination already exists -- - `AlreadyExists` if destination already exists
copyDirRecursive :: Path Abs -- ^ source dir copyDirRecursive :: Path b1 -- ^ source dir
-> Path Abs -- ^ destination (parent dirs -> Path b2 -- ^ destination (parent dirs
-- are not automatically created) -- are not automatically created)
-> CopyMode -> CopyMode
-> RecursiveErrorMode -> RecursiveErrorMode
-> IO () -> IO ()
@ -344,27 +349,27 @@ copyDirRecursive fromp destdirp cm rm
(throwIO . RecursiveFailure $ collectedExceptions) (throwIO . RecursiveFailure $ collectedExceptions)
where where
go :: IORef [(RecursiveFailureHint, IOException)] go :: IORef [(RecursiveFailureHint, IOException)]
-> Path Abs -> Path Abs -> IO () -> Path b1 -> Path b2 -> IO ()
go ce fromp' destdirp' = do go ce fromp'@(MkPath fromBS) destdirp'@(MkPath destdirpBS) = do
-- NOTE: order is important here, so we don't get empty directories -- NOTE: order is important here, so we don't get empty directories
-- on failure -- on failure
-- get the contents of the source dir -- get the contents of the source dir
contents <- handleIOE (ReadContentsFailed fromp' destdirp') ce [] $ do contents <- handleIOE (ReadContentsFailed fromBS destdirpBS) ce [] $ do
contents <- getDirsFiles fromp' contents <- getDirsFiles fromp'
-- create the destination dir and -- create the destination dir and
-- only return contents if we succeed -- only return contents if we succeed
handleIOE (CreateDirFailed fromp' destdirp') ce [] $ do handleIOE (CreateDirFailed fromBS destdirpBS) ce [] $ do
fmode' <- PF.fileMode <$> PF.getSymbolicLinkStatus (fromAbs fromp') fmode' <- PF.fileMode <$> PF.getSymbolicLinkStatus fromBS
case cm of case cm of
Strict -> createDirectory (fromAbs destdirp') fmode' Strict -> createDirectory destdirpBS fmode'
Overwrite -> catchIOError (createDirectory (fromAbs destdirp') Overwrite -> catchIOError (createDirectory destdirpBS
fmode') fmode')
$ \e -> $ \e ->
case ioeGetErrorType e of case ioeGetErrorType e of
AlreadyExists -> setFileMode (fromAbs destdirp') AlreadyExists -> setFileMode destdirpBS
fmode' fmode'
_ -> ioError e _ -> ioError e
return contents return contents
@ -378,10 +383,10 @@ copyDirRecursive fromp destdirp cm rm
ftype <- getFileType f ftype <- getFileType f
newdest <- (destdirp' </>) <$> basename f newdest <- (destdirp' </>) <$> basename f
case ftype of case ftype of
SymbolicLink -> handleIOE (RecreateSymlinkFailed f newdest) ce () SymbolicLink -> handleIOE (RecreateSymlinkFailed (toFilePath f) (toFilePath newdest)) ce ()
$ recreateSymlink f newdest cm $ recreateSymlink f newdest cm
Directory -> go ce f newdest Directory -> go ce f newdest
RegularFile -> handleIOE (CopyFileFailed f newdest) ce () RegularFile -> handleIOE (CopyFileFailed (toFilePath f) (toFilePath newdest)) ce ()
$ copyFile f newdest cm $ copyFile f newdest cm
_ -> return () _ -> return ()
@ -422,23 +427,23 @@ copyDirRecursive fromp destdirp cm rm
-- - `UnsatisfiedConstraints` if destination file is non-empty directory -- - `UnsatisfiedConstraints` if destination file is non-empty directory
-- --
-- Note: calls `symlink` -- Note: calls `symlink`
recreateSymlink :: Path Abs -- ^ the old symlink file recreateSymlink :: Path b1 -- ^ the old symlink file
-> Path Abs -- ^ destination file -> Path b2 -- ^ destination file
-> CopyMode -> CopyMode
-> IO () -> IO ()
recreateSymlink symsource newsym cm recreateSymlink symsource@(MkPath symsourceBS) newsym@(MkPath newsymBS) cm
= do = do
throwSameFile symsource newsym throwSameFile symsource newsym
sympoint <- readSymbolicLink (fromAbs symsource) sympoint <- readSymbolicLink symsourceBS
case cm of case cm of
Strict -> return () Strict -> return ()
Overwrite -> do Overwrite -> do
writable <- isWritable (dirname newsym) writable <- toAbs newsym >>= isWritable
isfile <- doesFileExist newsym isfile <- doesFileExist newsym
isdir <- doesDirectoryExist newsym isdir <- doesDirectoryExist newsym
when (writable && isfile) (deleteFile newsym) when (writable && isfile) (deleteFile newsym)
when (writable && isdir) (deleteDir newsym) when (writable && isdir) (deleteDir newsym)
createSymbolicLink sympoint (fromAbs newsym) createSymbolicLink sympoint newsymBS
-- |Copies the given regular file to the given destination. -- |Copies the given regular file to the given destination.
@ -476,8 +481,8 @@ recreateSymlink symsource newsym cm
-- - `AlreadyExists` if destination already exists -- - `AlreadyExists` if destination already exists
-- --
-- Note: calls `sendfile` and possibly `read`/`write` as fallback -- Note: calls `sendfile` and possibly `read`/`write` as fallback
copyFile :: Path Abs -- ^ source file copyFile :: Path b1 -- ^ source file
-> Path Abs -- ^ destination file -> Path b2 -- ^ destination file
-> CopyMode -> CopyMode
-> IO () -> IO ()
copyFile from to cm = do copyFile from to cm = do
@ -496,8 +501,8 @@ copyFile from to cm = do
-- figure out if we can still copy by deleting it first -- figure out if we can still copy by deleting it first
PermissionDenied -> do PermissionDenied -> do
exists <- doesFileExist to exists <- doesFileExist to
writable <- isWritable (dirname to) writable <- toAbs to >>= isWritable
if exists && writable if (exists && writable)
then deleteFile to >> copyFile from to Strict then deleteFile to >> copyFile from to Strict
else ioError e else ioError e
_ -> ioError e _ -> ioError e
@ -505,18 +510,17 @@ copyFile from to cm = do
_copyFile :: [SPDF.Flags] _copyFile :: [SPDF.Flags]
-> [SPDF.Flags] -> [SPDF.Flags]
-> Path Abs -- ^ source file -> Path b1 -- ^ source file
-> Path Abs -- ^ destination file -> Path b2 -- ^ destination file
-> IO () -> IO ()
_copyFile sflags dflags from to _copyFile sflags dflags (MkPath fromBS) to@(MkPath toBS)
= =
-- from sendfile(2) manpage: -- from sendfile(2) manpage:
-- Applications may wish to fall back to read(2)/write(2) in -- Applications may wish to fall back to read(2)/write(2) in
-- the case where sendfile() fails with EINVAL or ENOSYS. -- the case where sendfile() fails with EINVAL or ENOSYS.
withAbsPath to $ \to' -> withAbsPath from $ \from' -> catchErrno [eINVAL, eNOSYS]
catchErrno [eINVAL, eNOSYS] (sendFileCopy fromBS toBS)
(sendFileCopy from' to') (void $ readWriteCopy fromBS toBS)
(void $ readWriteCopy from' to')
where where
copyWith copyAction source dest = copyWith copyAction source dest =
bracket (openFd source SPI.ReadOnly sflags Nothing) bracket (openFd source SPI.ReadOnly sflags Nothing)
@ -561,8 +565,8 @@ _copyFile sflags dflags from to
-- --
-- * examines filetypes explicitly -- * examines filetypes explicitly
-- * calls `copyDirRecursive` for directories -- * calls `copyDirRecursive` for directories
easyCopy :: Path Abs easyCopy :: Path b1
-> Path Abs -> Path b2
-> CopyMode -> CopyMode
-> RecursiveErrorMode -> RecursiveErrorMode
-> IO () -> IO ()
@ -591,8 +595,8 @@ easyCopy from to cm rm = do
-- - `InappropriateType` for wrong file type (directory) -- - `InappropriateType` for wrong file type (directory)
-- - `NoSuchThing` if the file does not exist -- - `NoSuchThing` if the file does not exist
-- - `PermissionDenied` if the directory cannot be read -- - `PermissionDenied` if the directory cannot be read
deleteFile :: Path Abs -> IO () deleteFile :: Path b -> IO ()
deleteFile p = withAbsPath p removeLink deleteFile (MkPath p) = removeLink p
-- |Deletes the given directory, which must be empty, never symlinks. -- |Deletes the given directory, which must be empty, never symlinks.
@ -606,8 +610,8 @@ deleteFile p = withAbsPath p removeLink
-- - `PermissionDenied` if we can't open or write to parent directory -- - `PermissionDenied` if we can't open or write to parent directory
-- --
-- Notes: calls `rmdir` -- Notes: calls `rmdir`
deleteDir :: Path Abs -> IO () deleteDir :: Path b -> IO ()
deleteDir p = withAbsPath p removeDirectory deleteDir (MkPath p) = removeDirectory p
-- |Deletes the given directory recursively. Does not follow symbolic -- |Deletes the given directory recursively. Does not follow symbolic
@ -629,7 +633,7 @@ deleteDir p = withAbsPath p removeDirectory
-- - `InappropriateType` for wrong file type (regular file) -- - `InappropriateType` for wrong file type (regular file)
-- - `NoSuchThing` if directory does not exist -- - `NoSuchThing` if directory does not exist
-- - `PermissionDenied` if we can't open or write to parent directory -- - `PermissionDenied` if we can't open or write to parent directory
deleteDirRecursive :: Path Abs -> IO () deleteDirRecursive :: Path b -> IO ()
deleteDirRecursive p = deleteDirRecursive p =
catchErrno [eNOTEMPTY, eEXIST] catchErrno [eNOTEMPTY, eEXIST]
(deleteDir p) (deleteDir p)
@ -654,7 +658,7 @@ deleteDirRecursive p =
-- --
-- * examines filetypes explicitly -- * examines filetypes explicitly
-- * calls `deleteDirRecursive` for directories -- * calls `deleteDirRecursive` for directories
easyDelete :: Path Abs -> IO () easyDelete :: Path b -> IO ()
easyDelete p = do easyDelete p = do
ftype <- getFileType p ftype <- getFileType p
case ftype of case ftype of
@ -673,21 +677,18 @@ easyDelete p = do
-- |Opens a file appropriately by invoking xdg-open. The file type -- |Opens a file appropriately by invoking xdg-open. The file type
-- is not checked. This forks a process. -- is not checked. This forks a process.
openFile :: Path Abs openFile :: Path b
-> IO ProcessID -> IO ProcessID
openFile p = openFile (MkPath fp) =
withAbsPath p $ \fp -> SPP.forkProcess $ SPP.executeFile "xdg-open" True [fp] Nothing
SPP.forkProcess $ SPP.executeFile "xdg-open" True [fp] Nothing
-- |Executes a program with the given arguments. This forks a process. -- |Executes a program with the given arguments. This forks a process.
executeFile :: Path Abs -- ^ program executeFile :: Path b -- ^ program
-> [ByteString] -- ^ arguments -> [ByteString] -- ^ arguments
-> IO ProcessID -> IO ProcessID
executeFile fp args executeFile (MkPath fp) args =
= withAbsPath fp $ \fpb -> SPP.forkProcess $ SPP.executeFile fp True args Nothing
SPP.forkProcess
$ SPP.executeFile fpb True args Nothing
@ -706,9 +707,9 @@ executeFile fp args
-- - `AlreadyExists` if destination already exists -- - `AlreadyExists` if destination already exists
-- - `NoSuchThing` if any of the parent components of the path -- - `NoSuchThing` if any of the parent components of the path
-- do not exist -- do not exist
createRegularFile :: FileMode -> Path Abs -> IO () createRegularFile :: FileMode -> Path b -> IO ()
createRegularFile fm dest = createRegularFile fm (MkPath destBS) =
bracket (SPI.openFd (fromAbs dest) SPI.WriteOnly (Just fm) bracket (SPI.openFd destBS SPI.WriteOnly (Just fm)
(SPI.defaultFileFlags { exclusive = True })) (SPI.defaultFileFlags { exclusive = True }))
SPI.closeFd SPI.closeFd
(\_ -> return ()) (\_ -> return ())
@ -722,8 +723,8 @@ createRegularFile fm dest =
-- - `AlreadyExists` if destination already exists -- - `AlreadyExists` if destination already exists
-- - `NoSuchThing` if any of the parent components of the path -- - `NoSuchThing` if any of the parent components of the path
-- do not exist -- do not exist
createDir :: FileMode -> Path Abs -> IO () createDir :: FileMode -> Path b -> IO ()
createDir fm dest = createDirectory (fromAbs dest) fm createDir fm (MkPath destBS) = createDirectory destBS fm
-- |Create an empty directory at the given directory with the given filename. -- |Create an empty directory at the given directory with the given filename.
@ -744,15 +745,19 @@ createDir fm dest = createDirectory (fromAbs dest) fm
-- exist and cannot be written to -- exist and cannot be written to
-- - `AlreadyExists` if destination already exists and -- - `AlreadyExists` if destination already exists and
-- is not a directory -- is not a directory
createDirRecursive :: FileMode -> Path Abs -> IO () createDirRecursive :: FileMode -> Path b -> IO ()
createDirRecursive fm dest = createDirRecursive fm p =
catchIOError (createDirectory (fromAbs dest) fm) $ \e -> do toAbs p >>= go
errno <- getErrno where
case errno of go :: Path Abs -> IO ()
en | en == eEXIST -> unlessM (doesDirectoryExist dest) (ioError e) go dest@(MkPath destBS) = do
| en == eNOENT -> createDirRecursive fm (dirname dest) catchIOError (createDirectory destBS fm) $ \e -> do
>> createDirectory (fromAbs dest) fm errno <- getErrno
| otherwise -> ioError e case errno of
en | en == eEXIST -> unlessM (doesDirectoryExist dest) (ioError e)
| en == eNOENT -> createDirRecursive fm (dirname dest)
>> createDirectory destBS fm
| otherwise -> ioError e
-- |Create a symlink. -- |Create a symlink.
@ -765,11 +770,11 @@ createDirRecursive fm dest =
-- do not exist -- do not exist
-- --
-- Note: calls `symlink` -- Note: calls `symlink`
createSymlink :: Path Abs -- ^ destination file createSymlink :: Path b -- ^ destination file
-> ByteString -- ^ path the symlink points to -> ByteString -- ^ path the symlink points to
-> IO () -> IO ()
createSymlink dest sympoint createSymlink (MkPath destBS) sympoint
= createSymbolicLink sympoint (fromAbs dest) = createSymbolicLink sympoint destBS
@ -799,12 +804,12 @@ createSymlink dest sympoint
-- (`HPathIOException`) -- (`HPathIOException`)
-- --
-- Note: calls `rename` (but does not allow to rename over existing files) -- Note: calls `rename` (but does not allow to rename over existing files)
renameFile :: Path Abs -> Path Abs -> IO () renameFile :: Path b1 -> Path b2 -> IO ()
renameFile fromf tof = do renameFile fromf@(MkPath fromfBS) tof@(MkPath tofBS) = do
throwSameFile fromf tof throwSameFile fromf tof
throwFileDoesExist tof throwFileDoesExist tof
throwDirDoesExist tof throwDirDoesExist tof
rename (fromAbs fromf) (fromAbs tof) rename fromfBS tofBS
-- |Move a file. This also works across devices by copy-delete fallback. -- |Move a file. This also works across devices by copy-delete fallback.
@ -836,8 +841,8 @@ renameFile fromf tof = do
-- - `AlreadyExists` if destination already exists -- - `AlreadyExists` if destination already exists
-- --
-- Note: calls `rename` (but does not allow to rename over existing files) -- Note: calls `rename` (but does not allow to rename over existing files)
moveFile :: Path Abs -- ^ file to move moveFile :: Path b1 -- ^ file to move
-> Path Abs -- ^ destination -> Path b2 -- ^ destination
-> CopyMode -> CopyMode
-> IO () -> IO ()
moveFile from to cm = do moveFile from to cm = do
@ -848,7 +853,7 @@ moveFile from to cm = do
easyDelete from easyDelete from
Overwrite -> do Overwrite -> do
ft <- getFileType from ft <- getFileType from
writable <- isWritable $ dirname to writable <- toAbs to >>= isWritable
case ft of case ft of
RegularFile -> do RegularFile -> do
exists <- doesFileExist to exists <- doesFileExist to
@ -891,8 +896,8 @@ moveFile from to cm = do
-- - `PermissionDenied` if we cannot read the file or the directory -- - `PermissionDenied` if we cannot read the file or the directory
-- containting it -- containting it
-- - `NoSuchThing` if the file does not exist -- - `NoSuchThing` if the file does not exist
readFile :: Path Abs -> IO ByteString readFile :: Path b -> IO ByteString
readFile p = withAbsPath p $ \fp -> readFile (MkPath fp) =
bracket (openFd fp SPI.ReadOnly [] Nothing) (SPI.closeFd) $ \fd -> do bracket (openFd fp SPI.ReadOnly [] Nothing) (SPI.closeFd) $ \fd -> do
stat <- PF.getFdStatus fd stat <- PF.getFdStatus fd
let fsize = PF.fileSize stat let fsize = PF.fileSize stat
@ -913,8 +918,8 @@ readFile p = withAbsPath p $ \fp ->
-- - `PermissionDenied` if we cannot read the file or the directory -- - `PermissionDenied` if we cannot read the file or the directory
-- containting it -- containting it
-- - `NoSuchThing` if the file does not exist -- - `NoSuchThing` if the file does not exist
readFileEOF :: Path Abs -> IO L.ByteString readFileEOF :: Path b -> IO L.ByteString
readFileEOF p = withAbsPath p $ \fp -> readFileEOF (MkPath fp) =
bracket (openFd fp SPI.ReadOnly [] Nothing) (SPI.closeFd) $ \fd -> bracket (openFd fp SPI.ReadOnly [] Nothing) (SPI.closeFd) $ \fd ->
allocaBytes (fromIntegral bufSize) $ \buf -> read' fd buf mempty allocaBytes (fromIntegral bufSize) $ \buf -> read' fd buf mempty
where where
@ -948,8 +953,8 @@ readFileEOF p = withAbsPath p $ \fp ->
-- - `PermissionDenied` if we cannot read the file or the directory -- - `PermissionDenied` if we cannot read the file or the directory
-- containting it -- containting it
-- - `NoSuchThing` if the file does not exist -- - `NoSuchThing` if the file does not exist
writeFile :: Path Abs -> ByteString -> IO () writeFile :: Path b -> ByteString -> IO ()
writeFile p bs = withAbsPath p $ \fp -> writeFile (MkPath fp) bs =
bracket (openFd fp SPI.WriteOnly [SPDF.oTrunc] Nothing) (SPI.closeFd) $ \fd -> bracket (openFd fp SPI.WriteOnly [SPDF.oTrunc] Nothing) (SPI.closeFd) $ \fd ->
void $ SPB.fdWrite fd bs void $ SPB.fdWrite fd bs
@ -963,8 +968,8 @@ writeFile p bs = withAbsPath p $ \fp ->
-- - `PermissionDenied` if we cannot read the file or the directory -- - `PermissionDenied` if we cannot read the file or the directory
-- containting it -- containting it
-- - `NoSuchThing` if the file does not exist -- - `NoSuchThing` if the file does not exist
appendFile :: Path Abs -> ByteString -> IO () appendFile :: Path b -> ByteString -> IO ()
appendFile p bs = withAbsPath p $ \fp -> appendFile (MkPath fp) bs =
bracket (openFd fp SPI.WriteOnly [SPDF.oAppend] Nothing) bracket (openFd fp SPI.WriteOnly [SPDF.oAppend] Nothing)
(SPI.closeFd) $ \fd -> void $ SPB.fdWrite fd bs (SPI.closeFd) $ \fd -> void $ SPB.fdWrite fd bs
@ -1015,15 +1020,14 @@ newDirPerms
-- - `InappropriateType` if file type is wrong (symlink to file) -- - `InappropriateType` if file type is wrong (symlink to file)
-- - `InappropriateType` if file type is wrong (symlink to dir) -- - `InappropriateType` if file type is wrong (symlink to dir)
-- - `PermissionDenied` if directory cannot be opened -- - `PermissionDenied` if directory cannot be opened
getDirsFiles :: Path Abs -- ^ dir to read getDirsFiles :: Path b -- ^ dir to read
-> IO [Path Abs] -> IO [Path b]
getDirsFiles p = getDirsFiles p@(MkPath fp) = do
withAbsPath p $ \fp -> do fd <- openFd fp SPI.ReadOnly [SPDF.oNofollow] Nothing
fd <- openFd fp SPI.ReadOnly [SPDF.oNofollow] Nothing return
return . catMaybes
. catMaybes . fmap (\x -> (</>) p <$> (parseMaybe . snd $ x))
. fmap (\x -> (</>) p <$> (parseMaybe . snd $ x)) =<< getDirectoryContents' fd
=<< getDirectoryContents' fd
where where
parseMaybe :: ByteString -> Maybe (Path Fn) parseMaybe :: ByteString -> Maybe (Path Fn)
parseMaybe = parseFn parseMaybe = parseFn
@ -1043,9 +1047,9 @@ getDirsFiles p =
-- --
-- - `NoSuchThing` if the file does not exist -- - `NoSuchThing` if the file does not exist
-- - `PermissionDenied` if any part of the path is not accessible -- - `PermissionDenied` if any part of the path is not accessible
getFileType :: Path Abs -> IO FileType getFileType :: Path b -> IO FileType
getFileType p = do getFileType (MkPath fp) = do
fs <- PF.getSymbolicLinkStatus (fromAbs p) fs <- PF.getSymbolicLinkStatus fp
decide fs decide fs
where where
decide fs decide fs
@ -1066,13 +1070,29 @@ getFileType p = do
-- |Applies `realpath` on the given absolute path. -- |Applies `realpath` on the given path.
-- --
-- Throws: -- Throws:
-- --
-- - `NoSuchThing` if the file at the given path does not exist -- - `NoSuchThing` if the file at the given path does not exist
-- - `NoSuchThing` if the symlink is broken -- - `NoSuchThing` if the symlink is broken
canonicalizePath :: Path Abs -> IO (Path Abs) canonicalizePath :: Path b -> IO (Path Abs)
canonicalizePath (MkPath l) = do canonicalizePath (MkPath l) = do
nl <- SPDT.realpath l nl <- SPDT.realpath l
return $ MkPath nl return $ MkPath nl
-- |Converts any path to an absolute path.
-- This is done in the following way:
--
-- - if the path is already an absolute one, just return it
-- - if it's a relative path, prepend the current directory to it
toAbs :: Path b -> IO (Path Abs)
toAbs (MkPath bs) = do
let mabs = parseAbs bs :: Maybe (Path Abs)
case mabs of
Just a -> return a
Nothing -> do
cwd <- getWorkingDirectory >>= parseAbs
rel <- parseRel bs -- we know it must be relative now
return $ cwd </> rel

View File

@ -3,5 +3,6 @@ module HPath.IO where
import HPath import HPath
canonicalizePath :: Path Abs -> IO (Path Abs) canonicalizePath :: Path b -> IO (Path Abs)
toAbs :: Path b -> IO (Path Abs)

View File

@ -84,9 +84,14 @@ import GHC.IO.Exception
IOErrorType IOErrorType
) )
import HPath import HPath
import HPath.Internal
(
Path(..)
)
import {-# SOURCE #-} HPath.IO import {-# SOURCE #-} HPath.IO
( (
canonicalizePath canonicalizePath
, toAbs
) )
import System.IO.Error import System.IO.Error
( (
@ -119,10 +124,10 @@ data HPathIOException = SameFile ByteString ByteString
-- --
-- The first argument to the data constructor is always the -- The first argument to the data constructor is always the
-- source and the second the destination. -- source and the second the destination.
data RecursiveFailureHint = ReadContentsFailed (Path Abs) (Path Abs) data RecursiveFailureHint = ReadContentsFailed ByteString ByteString
| CreateDirFailed (Path Abs) (Path Abs) | CreateDirFailed ByteString ByteString
| CopyFileFailed (Path Abs) (Path Abs) | CopyFileFailed ByteString ByteString
| RecreateSymlinkFailed (Path Abs) (Path Abs) | RecreateSymlinkFailed ByteString ByteString
deriving (Eq, Show) deriving (Eq, Show)
@ -169,51 +174,50 @@ isRecreateSymlinkFailed _ = False
-- |Throws `AlreadyExists` `IOError` if file exists. -- |Throws `AlreadyExists` `IOError` if file exists.
throwFileDoesExist :: Path Abs -> IO () throwFileDoesExist :: Path b -> IO ()
throwFileDoesExist fp = throwFileDoesExist fp@(MkPath bs) =
whenM (doesFileExist fp) whenM (doesFileExist fp)
(ioError . mkIOError (ioError . mkIOError
alreadyExistsErrorType alreadyExistsErrorType
"File already exists" "File already exists"
Nothing Nothing
$ (Just (toString $ fromAbs fp)) $ (Just (toString $ bs))
) )
-- |Throws `AlreadyExists` `IOError` if directory exists. -- |Throws `AlreadyExists` `IOError` if directory exists.
throwDirDoesExist :: Path Abs -> IO () throwDirDoesExist :: Path b -> IO ()
throwDirDoesExist fp = throwDirDoesExist fp@(MkPath bs) =
whenM (doesDirectoryExist fp) whenM (doesDirectoryExist fp)
(ioError . mkIOError (ioError . mkIOError
alreadyExistsErrorType alreadyExistsErrorType
"Directory already exists" "Directory already exists"
Nothing Nothing
$ (Just (toString $ fromAbs fp)) $ (Just (toString $ bs))
) )
-- |Uses `isSameFile` and throws `SameFile` if it returns True. -- |Uses `isSameFile` and throws `SameFile` if it returns True.
throwSameFile :: Path Abs throwSameFile :: Path b1
-> Path Abs -> Path b2
-> IO () -> IO ()
throwSameFile fp1 fp2 = throwSameFile fp1@(MkPath bs1) fp2@(MkPath bs2) =
whenM (sameFile fp1 fp2) whenM (sameFile fp1 fp2)
(throwIO $ SameFile (fromAbs fp1) (fromAbs fp2)) (throwIO $ SameFile bs1 bs2)
-- |Check if the files are the same by examining device and file id. -- |Check if the files are the same by examining device and file id.
-- This follows symbolic links. -- This follows symbolic links.
sameFile :: Path Abs -> Path Abs -> IO Bool sameFile :: Path b1 -> Path b2 -> IO Bool
sameFile fp1 fp2 = sameFile (MkPath fp1) (MkPath fp2) =
withAbsPath fp1 $ \fp1' -> withAbsPath fp2 $ \fp2' -> handleIOError (\_ -> return False) $ do
handleIOError (\_ -> return False) $ do fs1 <- getFileStatus fp1
fs1 <- getFileStatus fp1' fs2 <- getFileStatus fp2
fs2 <- getFileStatus fp2'
if ((PF.deviceID fs1, PF.fileID fs1) == if ((PF.deviceID fs1, PF.fileID fs1) ==
(PF.deviceID fs2, PF.fileID fs2)) (PF.deviceID fs2, PF.fileID fs2))
then return True then return True
else return False else return False
-- TODO: make this more robust when destination does not exist -- TODO: make this more robust when destination does not exist
@ -221,54 +225,54 @@ sameFile fp1 fp2 =
-- within the source directory by comparing the device+file ID of the -- within the source directory by comparing the device+file ID of the
-- source directory with all device+file IDs of the parent directories -- source directory with all device+file IDs of the parent directories
-- of the destination. -- of the destination.
throwDestinationInSource :: Path Abs -- ^ source dir throwDestinationInSource :: Path b1 -- ^ source dir
-> Path Abs -- ^ full destination, @dirname dest@ -> Path b2 -- ^ full destination, @dirname dest@
-- must exist -- must exist
-> IO () -> IO ()
throwDestinationInSource source dest = do throwDestinationInSource (MkPath sbs) dest@(MkPath dbs) = do
destAbs <- toAbs dest
dest' <- (\x -> maybe x (\y -> x </> y) $ basename dest) dest' <- (\x -> maybe x (\y -> x </> y) $ basename dest)
<$> (canonicalizePath $ dirname dest) <$> (canonicalizePath $ dirname destAbs)
dids <- forM (getAllParents dest') $ \p -> do dids <- forM (getAllParents dest') $ \p -> do
fs <- PF.getSymbolicLinkStatus (fromAbs p) fs <- PF.getSymbolicLinkStatus (fromAbs p)
return (PF.deviceID fs, PF.fileID fs) return (PF.deviceID fs, PF.fileID fs)
sid <- fmap (\x -> (PF.deviceID x, PF.fileID x)) sid <- fmap (\x -> (PF.deviceID x, PF.fileID x))
$ PF.getFileStatus (fromAbs source) $ PF.getFileStatus sbs
when (elem sid dids) when (elem sid dids)
(throwIO $ DestinationInSource (fromAbs dest) (throwIO $ DestinationInSource dbs sbs)
(fromAbs source))
-- |Checks if the given file exists and is not a directory. -- |Checks if the given file exists and is not a directory.
-- Does not follow symlinks. -- Does not follow symlinks.
doesFileExist :: Path Abs -> IO Bool doesFileExist :: Path b -> IO Bool
doesFileExist fp = doesFileExist (MkPath bs) =
handleIOError (\_ -> return False) $ do handleIOError (\_ -> return False) $ do
fs <- PF.getSymbolicLinkStatus (fromAbs fp) fs <- PF.getSymbolicLinkStatus bs
return $ not . PF.isDirectory $ fs return $ not . PF.isDirectory $ fs
-- |Checks if the given file exists and is a directory. -- |Checks if the given file exists and is a directory.
-- Does not follow symlinks. -- Does not follow symlinks.
doesDirectoryExist :: Path Abs -> IO Bool doesDirectoryExist :: Path b -> IO Bool
doesDirectoryExist fp = doesDirectoryExist (MkPath bs) =
handleIOError (\_ -> return False) $ do handleIOError (\_ -> return False) $ do
fs <- PF.getSymbolicLinkStatus (fromAbs fp) fs <- PF.getSymbolicLinkStatus bs
return $ PF.isDirectory fs return $ PF.isDirectory fs
-- |Checks whether a file or folder is writable. -- |Checks whether a file or folder is writable.
isWritable :: Path Abs -> IO Bool isWritable :: Path b -> IO Bool
isWritable fp = isWritable (MkPath bs) =
handleIOError (\_ -> return False) $ handleIOError (\_ -> return False) $
fileAccess (fromAbs fp) False True False fileAccess bs False True False
-- |Checks whether the directory at the given path exists and can be -- |Checks whether the directory at the given path exists and can be
-- opened. This invokes `openDirStream` which follows symlinks. -- opened. This invokes `openDirStream` which follows symlinks.
canOpenDirectory :: Path Abs -> IO Bool canOpenDirectory :: Path b -> IO Bool
canOpenDirectory fp = canOpenDirectory (MkPath bs) =
handleIOError (\_ -> return False) $ do handleIOError (\_ -> return False) $ do
bracket (PFD.openDirStream . fromAbs $ fp) bracket (PFD.openDirStream bs)
PFD.closeDirStream PFD.closeDirStream
(\_ -> return ()) (\_ -> return ())
return True return True

View File

@ -0,0 +1,27 @@
{-# LANGUAGE OverloadedStrings #-}
module HPath.IO.ToAbsSpec where
import Test.Hspec
import HPath
import HPath.IO
spec :: Spec
spec = describe "HPath.IO.toAbs" $ do
-- successes --
it "toAbs returns absolute paths unchanged" $ do
p1 <- parseAbs "/a/b/c/d"
to <- toAbs p1
p1 `shouldBe` to
it "toAbs returns even existing absolute paths unchanged" $ do
p1 <- parseAbs "/home"
to <- toAbs p1
p1 `shouldBe` to