You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

361 lines
9.9 KiB

  1. -- |
  2. -- Module : HPath.IO.Errors
  3. -- Copyright : © 2016 Julian Ospald
  4. -- License : BSD3
  5. --
  6. -- Maintainer : Julian Ospald <hasufell@posteo.de>
  7. -- Stability : experimental
  8. -- Portability : portable
  9. --
  10. -- Provides error handling.
  11. {-# LANGUAGE DeriveDataTypeable #-}
  12. {-# LANGUAGE ScopedTypeVariables #-}
  13. module HPath.IO.Errors
  14. (
  15. -- * Types
  16. HPathIOException(..)
  17. , RecursiveFailureHint(..)
  18. -- * Exception identifiers
  19. , isSameFile
  20. , isDestinationInSource
  21. , isRecursiveFailure
  22. , isReadContentsFailed
  23. , isCreateDirFailed
  24. , isCopyFileFailed
  25. , isRecreateSymlinkFailed
  26. -- * Path based functions
  27. , throwFileDoesExist
  28. , throwDirDoesExist
  29. , throwSameFile
  30. , sameFile
  31. , throwDestinationInSource
  32. , doesFileExist
  33. , doesDirectoryExist
  34. , isWritable
  35. , canOpenDirectory
  36. -- * Error handling functions
  37. , catchErrno
  38. , rethrowErrnoAs
  39. , handleIOError
  40. , bracketeer
  41. , reactOnError
  42. )
  43. where
  44. import Control.Applicative
  45. (
  46. (<$>)
  47. )
  48. import Control.Exception
  49. import Control.Monad
  50. (
  51. forM
  52. , when
  53. )
  54. import Control.Monad.IfElse
  55. (
  56. whenM
  57. )
  58. import Data.ByteString
  59. (
  60. ByteString
  61. )
  62. import Data.ByteString.UTF8
  63. (
  64. toString
  65. )
  66. import Data.Typeable
  67. (
  68. Typeable
  69. )
  70. import Foreign.C.Error
  71. (
  72. getErrno
  73. , Errno
  74. )
  75. import GHC.IO.Exception
  76. (
  77. IOErrorType
  78. )
  79. import HPath
  80. import HPath.Internal
  81. (
  82. Path(..)
  83. )
  84. import {-# SOURCE #-} HPath.IO
  85. (
  86. canonicalizePath
  87. , toAbs
  88. )
  89. import System.IO.Error
  90. (
  91. alreadyExistsErrorType
  92. , catchIOError
  93. , ioeGetErrorType
  94. , mkIOError
  95. )
  96. import qualified System.Posix.Directory.ByteString as PFD
  97. import System.Posix.Files.ByteString
  98. (
  99. fileAccess
  100. , getFileStatus
  101. )
  102. import qualified System.Posix.Files.ByteString as PF
  103. -- |Additional generic IO exceptions that the posix functions
  104. -- do not provide.
  105. data HPathIOException = SameFile ByteString ByteString
  106. | DestinationInSource ByteString ByteString
  107. | RecursiveFailure [(RecursiveFailureHint, IOException)]
  108. deriving (Eq, Show, Typeable)
  109. -- |A type for giving failure hints on recursive failure, which allows
  110. -- to programmatically make choices without examining
  111. -- the weakly typed I/O error attributes (like `ioeGetFileName`).
  112. --
  113. -- The first argument to the data constructor is always the
  114. -- source and the second the destination.
  115. data RecursiveFailureHint = ReadContentsFailed ByteString ByteString
  116. | CreateDirFailed ByteString ByteString
  117. | CopyFileFailed ByteString ByteString
  118. | RecreateSymlinkFailed ByteString ByteString
  119. deriving (Eq, Show)
  120. instance Exception HPathIOException
  121. toConstr :: HPathIOException -> String
  122. toConstr SameFile {} = "SameFile"
  123. toConstr DestinationInSource {} = "DestinationInSource"
  124. toConstr RecursiveFailure {} = "RecursiveFailure"
  125. -----------------------------
  126. --[ Exception identifiers ]--
  127. -----------------------------
  128. isSameFile, isDestinationInSource, isRecursiveFailure :: HPathIOException -> Bool
  129. isSameFile ex = toConstr (ex :: HPathIOException) == toConstr (SameFile mempty mempty)
  130. isDestinationInSource ex = toConstr (ex :: HPathIOException) == (toConstr $ DestinationInSource mempty mempty)
  131. isRecursiveFailure ex = toConstr (ex :: HPathIOException) == (toConstr $ RecursiveFailure mempty)
  132. isReadContentsFailed, isCreateDirFailed, isCopyFileFailed, isRecreateSymlinkFailed ::RecursiveFailureHint -> Bool
  133. isReadContentsFailed ReadContentsFailed{} = True
  134. isReadContentsFailed _ = False
  135. isCreateDirFailed CreateDirFailed{} = True
  136. isCreateDirFailed _ = False
  137. isCopyFileFailed CopyFileFailed{} = True
  138. isCopyFileFailed _ = False
  139. isRecreateSymlinkFailed RecreateSymlinkFailed{} = True
  140. isRecreateSymlinkFailed _ = False
  141. ----------------------------
  142. --[ Path based functions ]--
  143. ----------------------------
  144. -- |Throws `AlreadyExists` `IOError` if file exists.
  145. throwFileDoesExist :: Path b -> IO ()
  146. throwFileDoesExist fp@(MkPath bs) =
  147. whenM (doesFileExist fp)
  148. (ioError . mkIOError
  149. alreadyExistsErrorType
  150. "File already exists"
  151. Nothing
  152. $ (Just (toString $ bs))
  153. )
  154. -- |Throws `AlreadyExists` `IOError` if directory exists.
  155. throwDirDoesExist :: Path b -> IO ()
  156. throwDirDoesExist fp@(MkPath bs) =
  157. whenM (doesDirectoryExist fp)
  158. (ioError . mkIOError
  159. alreadyExistsErrorType
  160. "Directory already exists"
  161. Nothing
  162. $ (Just (toString $ bs))
  163. )
  164. -- |Uses `isSameFile` and throws `SameFile` if it returns True.
  165. throwSameFile :: Path b1
  166. -> Path b2
  167. -> IO ()
  168. throwSameFile fp1@(MkPath bs1) fp2@(MkPath bs2) =
  169. whenM (sameFile fp1 fp2)
  170. (throwIO $ SameFile bs1 bs2)
  171. -- |Check if the files are the same by examining device and file id.
  172. -- This follows symbolic links.
  173. sameFile :: Path b1 -> Path b2 -> IO Bool
  174. sameFile (MkPath fp1) (MkPath fp2) =
  175. handleIOError (\_ -> return False) $ do
  176. fs1 <- getFileStatus fp1
  177. fs2 <- getFileStatus fp2
  178. if ((PF.deviceID fs1, PF.fileID fs1) ==
  179. (PF.deviceID fs2, PF.fileID fs2))
  180. then return True
  181. else return False
  182. -- TODO: make this more robust when destination does not exist
  183. -- |Checks whether the destination directory is contained
  184. -- within the source directory by comparing the device+file ID of the
  185. -- source directory with all device+file IDs of the parent directories
  186. -- of the destination.
  187. throwDestinationInSource :: Path b1 -- ^ source dir
  188. -> Path b2 -- ^ full destination, @dirname dest@
  189. -- must exist
  190. -> IO ()
  191. throwDestinationInSource (MkPath sbs) dest@(MkPath dbs) = do
  192. destAbs <- toAbs dest
  193. dest' <- (\x -> maybe x (\y -> x </> y) $ basename dest)
  194. <$> (canonicalizePath $ dirname destAbs)
  195. dids <- forM (getAllParents dest') $ \p -> do
  196. fs <- PF.getSymbolicLinkStatus (fromAbs p)
  197. return (PF.deviceID fs, PF.fileID fs)
  198. sid <- fmap (\x -> (PF.deviceID x, PF.fileID x))
  199. $ PF.getFileStatus sbs
  200. when (elem sid dids)
  201. (throwIO $ DestinationInSource dbs sbs)
  202. -- |Checks if the given file exists and is not a directory.
  203. -- Does not follow symlinks.
  204. doesFileExist :: Path b -> IO Bool
  205. doesFileExist (MkPath bs) =
  206. handleIOError (\_ -> return False) $ do
  207. fs <- PF.getSymbolicLinkStatus bs
  208. return $ not . PF.isDirectory $ fs
  209. -- |Checks if the given file exists and is a directory.
  210. -- Does not follow symlinks.
  211. doesDirectoryExist :: Path b -> IO Bool
  212. doesDirectoryExist (MkPath bs) =
  213. handleIOError (\_ -> return False) $ do
  214. fs <- PF.getSymbolicLinkStatus bs
  215. return $ PF.isDirectory fs
  216. -- |Checks whether a file or folder is writable.
  217. isWritable :: Path b -> IO Bool
  218. isWritable (MkPath bs) =
  219. handleIOError (\_ -> return False) $
  220. fileAccess bs False True False
  221. -- |Checks whether the directory at the given path exists and can be
  222. -- opened. This invokes `openDirStream` which follows symlinks.
  223. canOpenDirectory :: Path b -> IO Bool
  224. canOpenDirectory (MkPath bs) =
  225. handleIOError (\_ -> return False) $ do
  226. bracket (PFD.openDirStream bs)
  227. PFD.closeDirStream
  228. (\_ -> return ())
  229. return True
  230. --------------------------------
  231. --[ Error handling functions ]--
  232. --------------------------------
  233. -- |Carries out an action, then checks if there is an IOException and
  234. -- a specific errno. If so, then it carries out another action, otherwise
  235. -- it rethrows the error.
  236. catchErrno :: [Errno] -- ^ errno to catch
  237. -> IO a -- ^ action to try, which can raise an IOException
  238. -> IO a -- ^ action to carry out in case of an IOException and
  239. -- if errno matches
  240. -> IO a
  241. catchErrno en a1 a2 =
  242. catchIOError a1 $ \e -> do
  243. errno <- getErrno
  244. if errno `elem` en
  245. then a2
  246. else ioError e
  247. -- |Execute the given action and retrow IO exceptions as a new Exception
  248. -- that have the given errno. If errno does not match the exception is rethrown
  249. -- as is.
  250. rethrowErrnoAs :: Exception e
  251. => [Errno] -- ^ errno to catch
  252. -> e -- ^ rethrow as if errno matches
  253. -> IO a -- ^ action to try
  254. -> IO a
  255. rethrowErrnoAs en fmex action = catchErrno en action (throwIO fmex)
  256. -- |Like `catchIOError`, with arguments swapped.
  257. handleIOError :: (IOError -> IO a) -> IO a -> IO a
  258. handleIOError = flip catchIOError
  259. -- |Like `bracket`, but allows to have different clean-up
  260. -- actions depending on whether the in-between computation
  261. -- has raised an exception or not.
  262. bracketeer :: IO a -- ^ computation to run first
  263. -> (a -> IO b) -- ^ computation to run last, when
  264. -- no exception was raised
  265. -> (a -> IO b) -- ^ computation to run last,
  266. -- when an exception was raised
  267. -> (a -> IO c) -- ^ computation to run in-between
  268. -> IO c
  269. bracketeer before after afterEx thing =
  270. mask $ \restore -> do
  271. a <- before
  272. r <- restore (thing a) `onException` afterEx a
  273. _ <- after a
  274. return r
  275. reactOnError :: IO a
  276. -> [(IOErrorType, IO a)] -- ^ reaction on IO errors
  277. -> [(HPathIOException, IO a)] -- ^ reaction on HPathIOException
  278. -> IO a
  279. reactOnError a ios fmios =
  280. a `catches` [iohandler, fmiohandler]
  281. where
  282. iohandler = Handler $
  283. \(ex :: IOException) ->
  284. foldr (\(t, a') y -> if ioeGetErrorType ex == t
  285. then a'
  286. else y)
  287. (throwIO ex)
  288. ios
  289. fmiohandler = Handler $
  290. \(ex :: HPathIOException) ->
  291. foldr (\(t, a') y -> if toConstr ex == toConstr t
  292. then a'
  293. else y)
  294. (throwIO ex)
  295. fmios