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.
 
 
 
 

270 lines
7.9 KiB

  1. {-# LANGUAGE ForeignFunctionInterface #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. {-# LANGUAGE TupleSections #-}
  4. {-# LANGUAGE ViewPatterns #-}
  5. {-# OPTIONS_GHC -Wall #-}
  6. module System.Posix.Directory.Traversals (
  7. getDirectoryContents
  8. , getDirectoryContents'
  9. , allDirectoryContents
  10. , allDirectoryContents'
  11. , traverseDirectory
  12. -- lower-level stuff
  13. , readDirEnt
  14. , packDirStream
  15. , unpackDirStream
  16. , openFd
  17. , realpath
  18. ) where
  19. import Control.Applicative
  20. import Control.Monad
  21. import System.Posix.FilePath ((</>))
  22. import System.Posix.Directory.Foreign
  23. import qualified System.Posix as Posix
  24. import System.IO.Error
  25. import Control.Exception
  26. import qualified Data.ByteString.Char8 as BS
  27. import System.Posix.ByteString.FilePath
  28. import System.Posix.Directory.ByteString as PosixBS
  29. import System.Posix.Files.ByteString
  30. import System.IO.Unsafe
  31. import Unsafe.Coerce (unsafeCoerce)
  32. import Foreign.C.Error
  33. import Foreign.C.String
  34. import Foreign.C.Types
  35. import Foreign.Marshal.Alloc (alloca,allocaBytes)
  36. import Foreign.Ptr
  37. import Foreign.Storable
  38. ----------------------------------------------------------
  39. -- | Get all files from a directory and its subdirectories.
  40. --
  41. -- Upon entering a directory, 'allDirectoryContents' will get all entries
  42. -- strictly. However the returned list is lazy in that directories will only
  43. -- be accessed on demand.
  44. allDirectoryContents :: RawFilePath -> IO [RawFilePath]
  45. allDirectoryContents topdir = do
  46. namesAndTypes <- getDirectoryContents topdir
  47. let properNames = filter ((`notElem` [".", ".."]) . snd) namesAndTypes
  48. paths <- forM properNames $ \(typ,name) -> unsafeInterleaveIO $ do
  49. let path = topdir </> name
  50. case () of
  51. () | typ == dtDir -> allDirectoryContents path
  52. | typ == dtUnknown -> do
  53. isDir <- isDirectory <$> getFileStatus path
  54. if isDir
  55. then allDirectoryContents path
  56. else return [path]
  57. | otherwise -> return [path]
  58. return (topdir : concat paths)
  59. -- | Get all files from a directory and its subdirectories strictly.
  60. allDirectoryContents' :: RawFilePath -> IO [RawFilePath]
  61. allDirectoryContents' = fmap reverse . traverseDirectory (\acc fp -> return (fp:acc)) []
  62. -- this uses traverseDirectory because it's more efficient than forcing the
  63. -- lazy version.
  64. -- | Recursively apply the 'action' to the parent directory and all
  65. -- files/subdirectories.
  66. --
  67. -- This function allows for memory-efficient traversals.
  68. traverseDirectory :: (s -> RawFilePath -> IO s) -> s -> RawFilePath -> IO s
  69. traverseDirectory act s0 topdir = toploop
  70. where
  71. toploop = do
  72. isDir <- isDirectory <$> getFileStatus topdir
  73. s' <- act s0 topdir
  74. if isDir then actOnDirContents topdir s' loop
  75. else return s'
  76. loop typ path acc = do
  77. isDir <- case () of
  78. () | typ == dtDir -> return True
  79. | typ == dtUnknown -> isDirectory <$> getFileStatus path
  80. | otherwise -> return False
  81. if isDir
  82. then act acc path >>= \acc' -> actOnDirContents path acc' loop
  83. else act acc path
  84. actOnDirContents :: RawFilePath
  85. -> b
  86. -> (DirType -> RawFilePath -> b -> IO b)
  87. -> IO b
  88. actOnDirContents pathRelToTop b f =
  89. modifyIOError ((`ioeSetFileName` (BS.unpack pathRelToTop)) .
  90. (`ioeSetLocation` "findBSTypRel")) $ do
  91. bracket
  92. (openDirStream pathRelToTop)
  93. (Posix.closeDirStream)
  94. (\dirp -> loop dirp b)
  95. where
  96. loop dirp b' = do
  97. (typ,e) <- readDirEnt dirp
  98. if (e == "")
  99. then return b'
  100. else do
  101. if (e == "." || e == "..")
  102. then loop dirp b'
  103. else f typ (pathRelToTop </> e) b' >>= loop dirp
  104. ----------------------------------------------------------
  105. -- dodgy stuff
  106. type CDir = ()
  107. type CDirent = ()
  108. -- Posix doesn't export DirStream, so to re-use that type we need to use
  109. -- unsafeCoerce. It's just a newtype, so this is a legitimate usage.
  110. -- ugly trick.
  111. unpackDirStream :: DirStream -> Ptr CDir
  112. unpackDirStream = unsafeCoerce
  113. packDirStream :: Ptr CDir -> DirStream
  114. packDirStream = unsafeCoerce
  115. -- the __hscore_* functions are defined in the unix package. We can import them and let
  116. -- the linker figure it out.
  117. foreign import ccall unsafe "__hscore_readdir"
  118. c_readdir :: Ptr CDir -> Ptr (Ptr CDirent) -> IO CInt
  119. foreign import ccall unsafe "__hscore_free_dirent"
  120. c_freeDirEnt :: Ptr CDirent -> IO ()
  121. foreign import ccall unsafe "__hscore_d_name"
  122. c_name :: Ptr CDirent -> IO CString
  123. foreign import ccall unsafe "__posixdir_d_type"
  124. c_type :: Ptr CDirent -> IO DirType
  125. foreign import ccall "realpath"
  126. c_realpath :: CString -> CString -> IO CString
  127. foreign import ccall unsafe "fdopendir"
  128. c_fdopendir :: Posix.Fd -> IO (Ptr ())
  129. foreign import ccall unsafe "open"
  130. c_open :: CString -> CInt -> Posix.CMode -> IO CInt
  131. ----------------------------------------------------------
  132. -- less dodgy but still lower-level
  133. readDirEnt :: DirStream -> IO (DirType, RawFilePath)
  134. readDirEnt (unpackDirStream -> dirp) =
  135. alloca $ \ptr_dEnt -> loop ptr_dEnt
  136. where
  137. loop ptr_dEnt = do
  138. resetErrno
  139. r <- c_readdir dirp ptr_dEnt
  140. if (r == 0)
  141. then do
  142. dEnt <- peek ptr_dEnt
  143. if (dEnt == nullPtr)
  144. then return (dtUnknown,BS.empty)
  145. else do
  146. dName <- c_name dEnt >>= peekFilePath
  147. dType <- c_type dEnt
  148. c_freeDirEnt dEnt
  149. return (dType, dName)
  150. else do
  151. errno <- getErrno
  152. if (errno == eINTR)
  153. then loop ptr_dEnt
  154. else do
  155. let (Errno eo) = errno
  156. if (eo == 0)
  157. then return (dtUnknown,BS.empty)
  158. else throwErrno "readDirEnt"
  159. getDirectoryContents :: RawFilePath -> IO [(DirType, RawFilePath)]
  160. getDirectoryContents path =
  161. modifyIOError ((`ioeSetFileName` (BS.unpack path)) .
  162. (`ioeSetLocation` "System.Posix.Directory.Traversals.getDirectoryContents")) $ do
  163. bracket
  164. (PosixBS.openDirStream path)
  165. PosixBS.closeDirStream
  166. loop
  167. where
  168. loop dirp = do
  169. t@(_typ,e) <- readDirEnt dirp
  170. if BS.null e then return [] else do
  171. es <- loop dirp
  172. return (t:es)
  173. fdOpendir :: Posix.Fd -> IO DirStream
  174. fdOpendir fd =
  175. packDirStream <$> throwErrnoIfNull "fdOpendir" (c_fdopendir fd)
  176. getDirectoryContents' :: Posix.Fd -> IO [(DirType, RawFilePath)]
  177. getDirectoryContents' fd =
  178. bracket
  179. (fdOpendir fd)
  180. PosixBS.closeDirStream
  181. loop
  182. where
  183. loop dirp = do
  184. t@(_typ,e) <- readDirEnt dirp
  185. if BS.null e then return [] else do
  186. es <- loop dirp
  187. return (t:es)
  188. open_ :: CString
  189. -> Posix.OpenMode
  190. -> [Flags]
  191. -> Maybe Posix.FileMode
  192. -> IO Posix.Fd
  193. open_ str how optional_flags maybe_mode = do
  194. fd <- c_open str all_flags mode_w
  195. return (Posix.Fd fd)
  196. where
  197. all_flags = unionFlags $ optional_flags ++ [open_mode] ++ creat
  198. (creat, mode_w) = case maybe_mode of
  199. Nothing -> ([],0)
  200. Just x -> ([oCreat], x)
  201. open_mode = case how of
  202. Posix.ReadOnly -> oRdonly
  203. Posix.WriteOnly -> oWronly
  204. Posix.ReadWrite -> oRdwr
  205. -- |Open and optionally create this file. See 'System.Posix.Files'
  206. -- for information on how to use the 'FileMode' type.
  207. openFd :: RawFilePath
  208. -> Posix.OpenMode
  209. -> [Flags]
  210. -> Maybe Posix.FileMode
  211. -> IO Posix.Fd
  212. openFd name how optional_flags maybe_mode =
  213. withFilePath name $ \str ->
  214. throwErrnoPathIfMinus1Retry "openFd" name $
  215. open_ str how optional_flags maybe_mode
  216. -- | return the canonicalized absolute pathname
  217. --
  218. -- like canonicalizePath, but uses realpath(3)
  219. realpath :: RawFilePath -> IO RawFilePath
  220. realpath inp = do
  221. allocaBytes pathMax $ \tmp -> do
  222. void $ BS.useAsCString inp $ \cstr -> throwErrnoIfNull "realpath" $ c_realpath cstr tmp
  223. BS.packCString tmp