2016-05-09 14:53:31 +00:00
|
|
|
-- |
|
|
|
|
-- Module : HPath.IO.Errors
|
|
|
|
-- Copyright : © 2016 Julian Ospald
|
2016-06-03 22:20:41 +00:00
|
|
|
-- License : BSD3
|
2016-05-09 14:53:31 +00:00
|
|
|
--
|
|
|
|
-- Maintainer : Julian Ospald <hasufell@posteo.de>
|
|
|
|
-- Stability : experimental
|
|
|
|
-- Portability : portable
|
|
|
|
--
|
|
|
|
-- Provides error handling.
|
|
|
|
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
|
2016-05-09 22:35:33 +00:00
|
|
|
module HPath.IO.Errors
|
|
|
|
(
|
|
|
|
-- * Types
|
|
|
|
HPathIOException(..)
|
2016-06-14 17:13:25 +00:00
|
|
|
, RecursiveFailureHint(..)
|
2016-05-09 22:35:33 +00:00
|
|
|
|
|
|
|
-- * Exception identifiers
|
|
|
|
, isSameFile
|
|
|
|
, isDestinationInSource
|
2016-06-05 01:10:28 +00:00
|
|
|
, isRecursiveFailure
|
2016-06-14 17:13:25 +00:00
|
|
|
, isReadContentsFailed
|
|
|
|
, isCreateDirFailed
|
|
|
|
, isCopyFileFailed
|
|
|
|
, isRecreateSymlinkFailed
|
2016-05-09 22:35:33 +00:00
|
|
|
|
|
|
|
-- * Path based functions
|
|
|
|
, throwFileDoesExist
|
|
|
|
, throwDirDoesExist
|
|
|
|
, throwSameFile
|
|
|
|
, sameFile
|
|
|
|
, throwDestinationInSource
|
|
|
|
, doesFileExist
|
|
|
|
, doesDirectoryExist
|
|
|
|
, isWritable
|
|
|
|
, canOpenDirectory
|
|
|
|
|
|
|
|
-- * Error handling functions
|
|
|
|
, catchErrno
|
|
|
|
, rethrowErrnoAs
|
|
|
|
, handleIOError
|
|
|
|
, bracketeer
|
|
|
|
, reactOnError
|
|
|
|
)
|
|
|
|
where
|
2016-05-09 14:53:31 +00:00
|
|
|
|
|
|
|
|
2016-05-09 16:53:26 +00:00
|
|
|
import Control.Applicative
|
|
|
|
(
|
|
|
|
(<$>)
|
|
|
|
)
|
2016-05-09 14:53:31 +00:00
|
|
|
import Control.Exception
|
|
|
|
import Control.Monad
|
|
|
|
(
|
|
|
|
forM
|
|
|
|
, when
|
|
|
|
)
|
2016-06-05 19:52:52 +00:00
|
|
|
import Control.Monad.IfElse
|
|
|
|
(
|
|
|
|
whenM
|
|
|
|
)
|
2016-05-09 14:53:31 +00:00
|
|
|
import Data.ByteString
|
|
|
|
(
|
|
|
|
ByteString
|
|
|
|
)
|
2016-05-18 02:11:40 +00:00
|
|
|
import Data.ByteString.UTF8
|
|
|
|
(
|
|
|
|
toString
|
|
|
|
)
|
2016-06-14 17:21:03 +00:00
|
|
|
import Data.Typeable
|
|
|
|
(
|
|
|
|
Typeable
|
|
|
|
)
|
2016-05-09 14:53:31 +00:00
|
|
|
import Foreign.C.Error
|
|
|
|
(
|
|
|
|
getErrno
|
|
|
|
, Errno
|
|
|
|
)
|
|
|
|
import GHC.IO.Exception
|
|
|
|
(
|
|
|
|
IOErrorType
|
|
|
|
)
|
|
|
|
import HPath
|
2016-05-09 22:11:42 +00:00
|
|
|
import {-# SOURCE #-} HPath.IO
|
|
|
|
(
|
|
|
|
canonicalizePath
|
|
|
|
)
|
2016-05-09 14:53:31 +00:00
|
|
|
import System.IO.Error
|
|
|
|
(
|
2016-06-14 17:13:25 +00:00
|
|
|
alreadyExistsErrorType
|
|
|
|
, catchIOError
|
2016-05-09 14:53:31 +00:00
|
|
|
, ioeGetErrorType
|
2016-06-14 17:13:25 +00:00
|
|
|
, mkIOError
|
2016-05-09 14:53:31 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
import qualified System.Posix.Directory.ByteString as PFD
|
|
|
|
import System.Posix.Files.ByteString
|
|
|
|
(
|
|
|
|
fileAccess
|
|
|
|
, getFileStatus
|
|
|
|
)
|
|
|
|
import qualified System.Posix.Files.ByteString as PF
|
|
|
|
|
|
|
|
|
2016-06-14 17:13:25 +00:00
|
|
|
-- |Additional generic IO exceptions that the posix functions
|
|
|
|
-- do not provide.
|
|
|
|
data HPathIOException = SameFile ByteString ByteString
|
2016-05-09 14:53:31 +00:00
|
|
|
| DestinationInSource ByteString ByteString
|
2016-06-14 17:13:25 +00:00
|
|
|
| RecursiveFailure [(RecursiveFailureHint, IOException)]
|
2016-06-14 17:21:03 +00:00
|
|
|
deriving (Eq, Show, Typeable)
|
2016-06-05 01:10:28 +00:00
|
|
|
|
|
|
|
|
2016-06-14 17:13:25 +00:00
|
|
|
-- |A type for giving failure hints on recursive failure, which allows
|
|
|
|
-- to programmatically make choices without examining
|
|
|
|
-- the weakly typed I/O error attributes (like `ioeGetFileName`).
|
|
|
|
--
|
|
|
|
-- The first argument to the data constructor is always the
|
|
|
|
-- source and the second the destination.
|
|
|
|
data RecursiveFailureHint = ReadContentsFailed (Path Abs) (Path Abs)
|
|
|
|
| CreateDirFailed (Path Abs) (Path Abs)
|
|
|
|
| CopyFileFailed (Path Abs) (Path Abs)
|
|
|
|
| RecreateSymlinkFailed (Path Abs) (Path Abs)
|
|
|
|
deriving (Eq, Show)
|
2016-06-05 01:10:28 +00:00
|
|
|
|
2016-05-09 14:53:31 +00:00
|
|
|
|
2016-06-14 17:13:25 +00:00
|
|
|
instance Exception HPathIOException
|
2016-05-09 14:53:31 +00:00
|
|
|
|
|
|
|
|
2016-06-14 17:13:25 +00:00
|
|
|
toConstr :: HPathIOException -> String
|
|
|
|
toConstr SameFile {} = "SameFile"
|
|
|
|
toConstr DestinationInSource {} = "DestinationInSource"
|
|
|
|
toConstr RecursiveFailure {} = "RecursiveFailure"
|
2016-05-09 14:53:31 +00:00
|
|
|
|
|
|
|
|
2016-05-09 22:35:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-----------------------------
|
|
|
|
--[ Exception identifiers ]--
|
|
|
|
-----------------------------
|
|
|
|
|
2016-06-14 17:13:25 +00:00
|
|
|
|
|
|
|
isSameFile, isDestinationInSource, isRecursiveFailure :: HPathIOException -> Bool
|
2016-05-09 22:13:14 +00:00
|
|
|
isSameFile ex = toConstr (ex :: HPathIOException) == toConstr SameFile{}
|
|
|
|
isDestinationInSource ex = toConstr (ex :: HPathIOException) == toConstr DestinationInSource{}
|
2016-06-05 01:10:28 +00:00
|
|
|
isRecursiveFailure ex = toConstr (ex :: HPathIOException) == toConstr RecursiveFailure{}
|
2016-05-09 14:53:31 +00:00
|
|
|
|
|
|
|
|
2016-06-14 17:13:25 +00:00
|
|
|
isReadContentsFailed, isCreateDirFailed, isCopyFileFailed, isRecreateSymlinkFailed ::RecursiveFailureHint -> Bool
|
|
|
|
isReadContentsFailed ReadContentsFailed{} = True
|
|
|
|
isReadContentsFailed _ = False
|
|
|
|
isCreateDirFailed CreateDirFailed{} = True
|
|
|
|
isCreateDirFailed _ = False
|
|
|
|
isCopyFileFailed CopyFileFailed{} = True
|
|
|
|
isCopyFileFailed _ = False
|
|
|
|
isRecreateSymlinkFailed RecreateSymlinkFailed{} = True
|
|
|
|
isRecreateSymlinkFailed _ = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2016-05-09 14:53:31 +00:00
|
|
|
|
|
|
|
----------------------------
|
|
|
|
--[ Path based functions ]--
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
|
2016-06-14 17:13:25 +00:00
|
|
|
-- |Throws `AlreadyExists` `IOError` if file exists.
|
2016-05-09 14:53:31 +00:00
|
|
|
throwFileDoesExist :: Path Abs -> IO ()
|
|
|
|
throwFileDoesExist fp =
|
2016-06-14 17:13:25 +00:00
|
|
|
whenM (doesFileExist fp)
|
|
|
|
(ioError . mkIOError
|
|
|
|
alreadyExistsErrorType
|
|
|
|
"File already exists"
|
|
|
|
Nothing
|
|
|
|
$ (Just (toString $ fromAbs fp))
|
|
|
|
)
|
2016-05-09 14:53:31 +00:00
|
|
|
|
|
|
|
|
2016-06-14 17:13:25 +00:00
|
|
|
-- |Throws `AlreadyExists` `IOError` if directory exists.
|
2016-05-09 14:53:31 +00:00
|
|
|
throwDirDoesExist :: Path Abs -> IO ()
|
|
|
|
throwDirDoesExist fp =
|
2016-06-14 17:13:25 +00:00
|
|
|
whenM (doesDirectoryExist fp)
|
|
|
|
(ioError . mkIOError
|
|
|
|
alreadyExistsErrorType
|
|
|
|
"Directory already exists"
|
|
|
|
Nothing
|
|
|
|
$ (Just (toString $ fromAbs fp))
|
|
|
|
)
|
2016-05-09 14:53:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
-- |Uses `isSameFile` and throws `SameFile` if it returns True.
|
|
|
|
throwSameFile :: Path Abs
|
|
|
|
-> Path Abs
|
|
|
|
-> IO ()
|
|
|
|
throwSameFile fp1 fp2 =
|
|
|
|
whenM (sameFile fp1 fp2)
|
|
|
|
(throwIO $ SameFile (fromAbs fp1) (fromAbs fp2))
|
|
|
|
|
|
|
|
|
|
|
|
-- |Check if the files are the same by examining device and file id.
|
|
|
|
-- This follows symbolic links.
|
|
|
|
sameFile :: Path Abs -> Path Abs -> IO Bool
|
|
|
|
sameFile fp1 fp2 =
|
|
|
|
withAbsPath fp1 $ \fp1' -> withAbsPath fp2 $ \fp2' ->
|
|
|
|
handleIOError (\_ -> return False) $ do
|
|
|
|
fs1 <- getFileStatus fp1'
|
|
|
|
fs2 <- getFileStatus fp2'
|
|
|
|
|
|
|
|
if ((PF.deviceID fs1, PF.fileID fs1) ==
|
|
|
|
(PF.deviceID fs2, PF.fileID fs2))
|
|
|
|
then return True
|
|
|
|
else return False
|
|
|
|
|
|
|
|
|
2016-05-24 01:25:27 +00:00
|
|
|
-- TODO: make this more robust when destination does not exist
|
2016-05-09 14:53:31 +00:00
|
|
|
-- |Checks whether the destination directory is contained
|
|
|
|
-- within the source directory by comparing the device+file ID of the
|
|
|
|
-- source directory with all device+file IDs of the parent directories
|
|
|
|
-- of the destination.
|
|
|
|
throwDestinationInSource :: Path Abs -- ^ source dir
|
2016-05-22 11:41:39 +00:00
|
|
|
-> Path Abs -- ^ full destination, @dirname dest@
|
2016-05-09 14:53:31 +00:00
|
|
|
-- must exist
|
|
|
|
-> IO ()
|
|
|
|
throwDestinationInSource source dest = do
|
|
|
|
dest' <- (\x -> maybe x (\y -> x </> y) $ basename dest)
|
2016-05-09 22:11:42 +00:00
|
|
|
<$> (canonicalizePath $ dirname dest)
|
2016-05-09 14:53:31 +00:00
|
|
|
dids <- forM (getAllParents dest') $ \p -> do
|
|
|
|
fs <- PF.getSymbolicLinkStatus (fromAbs p)
|
|
|
|
return (PF.deviceID fs, PF.fileID fs)
|
|
|
|
sid <- fmap (\x -> (PF.deviceID x, PF.fileID x))
|
|
|
|
$ PF.getFileStatus (fromAbs source)
|
|
|
|
when (elem sid dids)
|
|
|
|
(throwIO $ DestinationInSource (fromAbs dest)
|
|
|
|
(fromAbs source))
|
|
|
|
|
|
|
|
|
|
|
|
-- |Checks if the given file exists and is not a directory.
|
|
|
|
-- Does not follow symlinks.
|
|
|
|
doesFileExist :: Path Abs -> IO Bool
|
|
|
|
doesFileExist fp =
|
|
|
|
handleIOError (\_ -> return False) $ do
|
|
|
|
fs <- PF.getSymbolicLinkStatus (fromAbs fp)
|
|
|
|
return $ not . PF.isDirectory $ fs
|
|
|
|
|
|
|
|
|
|
|
|
-- |Checks if the given file exists and is a directory.
|
|
|
|
-- Does not follow symlinks.
|
|
|
|
doesDirectoryExist :: Path Abs -> IO Bool
|
|
|
|
doesDirectoryExist fp =
|
|
|
|
handleIOError (\_ -> return False) $ do
|
|
|
|
fs <- PF.getSymbolicLinkStatus (fromAbs fp)
|
|
|
|
return $ PF.isDirectory fs
|
|
|
|
|
|
|
|
|
|
|
|
-- |Checks whether a file or folder is writable.
|
|
|
|
isWritable :: Path Abs -> IO Bool
|
|
|
|
isWritable fp =
|
|
|
|
handleIOError (\_ -> return False) $
|
|
|
|
fileAccess (fromAbs fp) False True False
|
|
|
|
|
|
|
|
|
|
|
|
-- |Checks whether the directory at the given path exists and can be
|
|
|
|
-- opened. This invokes `openDirStream` which follows symlinks.
|
|
|
|
canOpenDirectory :: Path Abs -> IO Bool
|
|
|
|
canOpenDirectory fp =
|
|
|
|
handleIOError (\_ -> return False) $ do
|
|
|
|
bracket (PFD.openDirStream . fromAbs $ fp)
|
|
|
|
PFD.closeDirStream
|
|
|
|
(\_ -> return ())
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
--------------------------------
|
|
|
|
--[ Error handling functions ]--
|
|
|
|
--------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
-- |Carries out an action, then checks if there is an IOException and
|
|
|
|
-- a specific errno. If so, then it carries out another action, otherwise
|
|
|
|
-- it rethrows the error.
|
|
|
|
catchErrno :: [Errno] -- ^ errno to catch
|
|
|
|
-> IO a -- ^ action to try, which can raise an IOException
|
|
|
|
-> IO a -- ^ action to carry out in case of an IOException and
|
|
|
|
-- if errno matches
|
|
|
|
-> IO a
|
|
|
|
catchErrno en a1 a2 =
|
|
|
|
catchIOError a1 $ \e -> do
|
|
|
|
errno <- getErrno
|
|
|
|
if errno `elem` en
|
|
|
|
then a2
|
|
|
|
else ioError e
|
|
|
|
|
|
|
|
|
|
|
|
-- |Execute the given action and retrow IO exceptions as a new Exception
|
|
|
|
-- that have the given errno. If errno does not match the exception is rethrown
|
|
|
|
-- as is.
|
|
|
|
rethrowErrnoAs :: Exception e
|
|
|
|
=> [Errno] -- ^ errno to catch
|
|
|
|
-> e -- ^ rethrow as if errno matches
|
|
|
|
-> IO a -- ^ action to try
|
|
|
|
-> IO a
|
|
|
|
rethrowErrnoAs en fmex action = catchErrno en action (throwIO fmex)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- |Like `catchIOError`, with arguments swapped.
|
|
|
|
handleIOError :: (IOError -> IO a) -> IO a -> IO a
|
|
|
|
handleIOError = flip catchIOError
|
|
|
|
|
|
|
|
|
|
|
|
-- |Like `bracket`, but allows to have different clean-up
|
|
|
|
-- actions depending on whether the in-between computation
|
|
|
|
-- has raised an exception or not.
|
|
|
|
bracketeer :: IO a -- ^ computation to run first
|
|
|
|
-> (a -> IO b) -- ^ computation to run last, when
|
|
|
|
-- no exception was raised
|
|
|
|
-> (a -> IO b) -- ^ computation to run last,
|
|
|
|
-- when an exception was raised
|
|
|
|
-> (a -> IO c) -- ^ computation to run in-between
|
|
|
|
-> IO c
|
|
|
|
bracketeer before after afterEx thing =
|
|
|
|
mask $ \restore -> do
|
|
|
|
a <- before
|
|
|
|
r <- restore (thing a) `onException` afterEx a
|
|
|
|
_ <- after a
|
|
|
|
return r
|
|
|
|
|
|
|
|
|
|
|
|
reactOnError :: IO a
|
2016-05-10 00:02:05 +00:00
|
|
|
-> [(IOErrorType, IO a)] -- ^ reaction on IO errors
|
|
|
|
-> [(HPathIOException, IO a)] -- ^ reaction on HPathIOException
|
2016-05-09 14:53:31 +00:00
|
|
|
-> IO a
|
|
|
|
reactOnError a ios fmios =
|
|
|
|
a `catches` [iohandler, fmiohandler]
|
|
|
|
where
|
|
|
|
iohandler = Handler $
|
|
|
|
\(ex :: IOException) ->
|
|
|
|
foldr (\(t, a') y -> if ioeGetErrorType ex == t
|
|
|
|
then a'
|
|
|
|
else y)
|
|
|
|
(throwIO ex)
|
|
|
|
ios
|
|
|
|
fmiohandler = Handler $
|
|
|
|
\(ex :: HPathIOException) ->
|
|
|
|
foldr (\(t, a') y -> if toConstr ex == toConstr t
|
|
|
|
then a'
|
|
|
|
else y)
|
|
|
|
(throwIO ex)
|
|
|
|
fmios
|
2016-06-14 17:13:25 +00:00
|
|
|
|