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.
 
 
 
 

360 lines
11 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. -- * Exception identifiers
  18. , isFileDoesNotExist
  19. , isDirDoesNotExist
  20. , isSameFile
  21. , isDestinationInSource
  22. , isFileDoesExist
  23. , isDirDoesExist
  24. , isInvalidOperation
  25. , isCan'tOpenDirectory
  26. , isCopyFailed
  27. -- * Path based functions
  28. , throwFileDoesExist
  29. , throwDirDoesExist
  30. , throwFileDoesNotExist
  31. , throwDirDoesNotExist
  32. , throwSameFile
  33. , sameFile
  34. , throwDestinationInSource
  35. , doesFileExist
  36. , doesDirectoryExist
  37. , isWritable
  38. , canOpenDirectory
  39. , throwCantOpenDirectory
  40. -- * Error handling functions
  41. , catchErrno
  42. , rethrowErrnoAs
  43. , handleIOError
  44. , bracketeer
  45. , reactOnError
  46. )
  47. where
  48. import Control.Applicative
  49. (
  50. (<$>)
  51. )
  52. import Control.Exception
  53. import Control.Monad
  54. (
  55. forM
  56. , when
  57. )
  58. import Data.ByteString
  59. (
  60. ByteString
  61. )
  62. import Data.ByteString.UTF8
  63. (
  64. toString
  65. )
  66. import Data.Data
  67. (
  68. Data(..)
  69. )
  70. import Data.Typeable
  71. import Foreign.C.Error
  72. (
  73. getErrno
  74. , Errno
  75. )
  76. import GHC.IO.Exception
  77. (
  78. IOErrorType
  79. )
  80. import HPath
  81. import {-# SOURCE #-} HPath.IO
  82. (
  83. canonicalizePath
  84. )
  85. import HPath.IO.Utils
  86. import System.IO.Error
  87. (
  88. catchIOError
  89. , ioeGetErrorType
  90. )
  91. import qualified System.Posix.Directory.ByteString as PFD
  92. import System.Posix.Files.ByteString
  93. (
  94. fileAccess
  95. , getFileStatus
  96. )
  97. import qualified System.Posix.Files.ByteString as PF
  98. data HPathIOException = FileDoesNotExist ByteString
  99. | DirDoesNotExist ByteString
  100. | SameFile ByteString ByteString
  101. | DestinationInSource ByteString ByteString
  102. | FileDoesExist ByteString
  103. | DirDoesExist ByteString
  104. | InvalidOperation String
  105. | Can'tOpenDirectory ByteString
  106. | CopyFailed String
  107. deriving (Typeable, Eq, Data)
  108. instance Show HPathIOException where
  109. show (FileDoesNotExist fp) = "File does not exist:" ++ toString fp
  110. show (DirDoesNotExist fp) = "Directory does not exist: "
  111. ++ toString fp
  112. show (SameFile fp1 fp2) = toString fp1
  113. ++ " and " ++ toString fp2
  114. ++ " are the same file!"
  115. show (DestinationInSource fp1 fp2) = toString fp1
  116. ++ " is contained in "
  117. ++ toString fp2
  118. show (FileDoesExist fp) = "File does exist: " ++ toString fp
  119. show (DirDoesExist fp) = "Directory does exist: " ++ toString fp
  120. show (InvalidOperation str) = "Invalid operation: " ++ str
  121. show (Can'tOpenDirectory fp) = "Can't open directory: "
  122. ++ toString fp
  123. show (CopyFailed str) = "Copying failed: " ++ str
  124. instance Exception HPathIOException
  125. -----------------------------
  126. --[ Exception identifiers ]--
  127. -----------------------------
  128. isFileDoesNotExist, isDirDoesNotExist, isSameFile, isDestinationInSource, isFileDoesExist, isDirDoesExist, isInvalidOperation, isCan'tOpenDirectory, isCopyFailed :: HPathIOException -> Bool
  129. isFileDoesNotExist ex = toConstr (ex :: HPathIOException) == toConstr FileDoesNotExist{}
  130. isDirDoesNotExist ex = toConstr (ex :: HPathIOException) == toConstr DirDoesNotExist{}
  131. isSameFile ex = toConstr (ex :: HPathIOException) == toConstr SameFile{}
  132. isDestinationInSource ex = toConstr (ex :: HPathIOException) == toConstr DestinationInSource{}
  133. isFileDoesExist ex = toConstr (ex :: HPathIOException) == toConstr FileDoesExist{}
  134. isDirDoesExist ex = toConstr (ex :: HPathIOException) == toConstr DirDoesExist{}
  135. isInvalidOperation ex = toConstr (ex :: HPathIOException) == toConstr InvalidOperation{}
  136. isCan'tOpenDirectory ex = toConstr (ex :: HPathIOException) == toConstr Can'tOpenDirectory{}
  137. isCopyFailed ex = toConstr (ex :: HPathIOException) == toConstr CopyFailed{}
  138. ----------------------------
  139. --[ Path based functions ]--
  140. ----------------------------
  141. throwFileDoesExist :: Path Abs -> IO ()
  142. throwFileDoesExist fp =
  143. whenM (doesFileExist fp) (throwIO . FileDoesExist
  144. . fromAbs $ fp)
  145. throwDirDoesExist :: Path Abs -> IO ()
  146. throwDirDoesExist fp =
  147. whenM (doesDirectoryExist fp) (throwIO . DirDoesExist
  148. . fromAbs $ fp)
  149. throwFileDoesNotExist :: Path Abs -> IO ()
  150. throwFileDoesNotExist fp =
  151. unlessM (doesFileExist fp) (throwIO . FileDoesNotExist
  152. . fromAbs $ fp)
  153. throwDirDoesNotExist :: Path Abs -> IO ()
  154. throwDirDoesNotExist fp =
  155. unlessM (doesDirectoryExist fp) (throwIO . DirDoesNotExist
  156. . fromAbs $ fp)
  157. -- |Uses `isSameFile` and throws `SameFile` if it returns True.
  158. throwSameFile :: Path Abs
  159. -> Path Abs
  160. -> IO ()
  161. throwSameFile fp1 fp2 =
  162. whenM (sameFile fp1 fp2)
  163. (throwIO $ SameFile (fromAbs fp1) (fromAbs fp2))
  164. -- |Check if the files are the same by examining device and file id.
  165. -- This follows symbolic links.
  166. sameFile :: Path Abs -> Path Abs -> IO Bool
  167. sameFile fp1 fp2 =
  168. withAbsPath fp1 $ \fp1' -> withAbsPath fp2 $ \fp2' ->
  169. handleIOError (\_ -> return False) $ do
  170. fs1 <- getFileStatus fp1'
  171. fs2 <- getFileStatus fp2'
  172. if ((PF.deviceID fs1, PF.fileID fs1) ==
  173. (PF.deviceID fs2, PF.fileID fs2))
  174. then return True
  175. else return False
  176. -- TODO: make this more robust when destination does not exist
  177. -- |Checks whether the destination directory is contained
  178. -- within the source directory by comparing the device+file ID of the
  179. -- source directory with all device+file IDs of the parent directories
  180. -- of the destination.
  181. throwDestinationInSource :: Path Abs -- ^ source dir
  182. -> Path Abs -- ^ full destination, @dirname dest@
  183. -- must exist
  184. -> IO ()
  185. throwDestinationInSource source dest = do
  186. dest' <- (\x -> maybe x (\y -> x </> y) $ basename dest)
  187. <$> (canonicalizePath $ dirname dest)
  188. dids <- forM (getAllParents dest') $ \p -> do
  189. fs <- PF.getSymbolicLinkStatus (fromAbs p)
  190. return (PF.deviceID fs, PF.fileID fs)
  191. sid <- fmap (\x -> (PF.deviceID x, PF.fileID x))
  192. $ PF.getFileStatus (fromAbs source)
  193. when (elem sid dids)
  194. (throwIO $ DestinationInSource (fromAbs dest)
  195. (fromAbs source))
  196. -- |Checks if the given file exists and is not a directory.
  197. -- Does not follow symlinks.
  198. doesFileExist :: Path Abs -> IO Bool
  199. doesFileExist fp =
  200. handleIOError (\_ -> return False) $ do
  201. fs <- PF.getSymbolicLinkStatus (fromAbs fp)
  202. return $ not . PF.isDirectory $ fs
  203. -- |Checks if the given file exists and is a directory.
  204. -- Does not follow symlinks.
  205. doesDirectoryExist :: Path Abs -> IO Bool
  206. doesDirectoryExist fp =
  207. handleIOError (\_ -> return False) $ do
  208. fs <- PF.getSymbolicLinkStatus (fromAbs fp)
  209. return $ PF.isDirectory fs
  210. -- |Checks whether a file or folder is writable.
  211. isWritable :: Path Abs -> IO Bool
  212. isWritable fp =
  213. handleIOError (\_ -> return False) $
  214. fileAccess (fromAbs fp) False True False
  215. -- |Checks whether the directory at the given path exists and can be
  216. -- opened. This invokes `openDirStream` which follows symlinks.
  217. canOpenDirectory :: Path Abs -> IO Bool
  218. canOpenDirectory fp =
  219. handleIOError (\_ -> return False) $ do
  220. bracket (PFD.openDirStream . fromAbs $ fp)
  221. PFD.closeDirStream
  222. (\_ -> return ())
  223. return True
  224. -- |Throws a `Can'tOpenDirectory` HPathIOException if the directory at the given
  225. -- path cannot be opened.
  226. throwCantOpenDirectory :: Path Abs -> IO ()
  227. throwCantOpenDirectory fp =
  228. unlessM (canOpenDirectory fp)
  229. (throwIO . Can'tOpenDirectory . fromAbs $ fp)
  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