hsfm/src/Data/DirTree.hs

613 lines
15 KiB
Haskell
Raw Normal View History

2015-12-17 03:42:22 +00:00
{-# OPTIONS_HADDOCK ignore-exports #-}
-- |This module provides data types for representing directories/files
-- and related operations on it, mostly internal stuff, not actual IO actions.
--
-- It doesn't allow to represent the whole filesystem, since that's only
-- possible through IO laziness, which introduces too much internal state.
2015-12-17 03:42:22 +00:00
module Data.DirTree where
import Control.Applicative
(
(<*>)
, (<$>)
, (<|>)
, pure
)
import Control.Arrow
(
first
)
import Control.Exception
(
handle
)
import Control.Exception.Base
(
IOException
)
import Control.Monad.State.Lazy
(
)
import Data.Default
import Data.List
(
delete
, foldl'
, isPrefixOf
2015-12-17 03:42:22 +00:00
, sort
, sortBy
, (\\)
)
import Data.Maybe
(
fromMaybe
)
import Data.Ord
(
comparing
)
import Data.Time.Clock.POSIX
(
POSIXTime
, posixSecondsToUTCTime
)
import Data.Traversable
2015-12-17 03:42:22 +00:00
(
for
2015-12-17 03:42:22 +00:00
)
import Data.Word
(
Word64
)
import Safe
(
atDef
, initDef
)
2015-12-17 03:42:22 +00:00
import System.Directory
(
canonicalizePath
, doesFileExist
, executable
2015-12-17 03:42:22 +00:00
, getPermissions
, readable
2015-12-17 03:42:22 +00:00
, searchable
, writable
, Permissions
2015-12-17 03:42:22 +00:00
)
import System.FilePath
(
combine
, normalise
2015-12-17 03:42:22 +00:00
, equalFilePath
, joinPath
, splitDirectories
, takeFileName
2015-12-17 03:42:22 +00:00
, (</>)
)
import System.IO
(
IOMode
, Handle
, openFile
)
import System.IO.Error
(
ioeGetErrorType
, isDoesNotExistErrorType
)
import System.IO.Unsafe
(
unsafeInterleaveIO
)
import System.Locale
(
defaultTimeLocale
, rfc822DateFormat
)
import System.Posix.Types
(
DeviceID
, EpochTime
, FileID
, FileMode
, FileOffset
, GroupID
, LinkCount
, UserID
)
2015-12-17 03:42:22 +00:00
import qualified Data.Bitraversable as BT
import qualified Data.Bifunctor as BF
import qualified Data.Bifoldable as BFL
import qualified Data.Traversable as T
import qualified System.Posix.Files as PF
import qualified System.Posix.Directory as PFD
2015-12-17 03:42:22 +00:00
----------------------------
--[ BASE TYPES ]--
----------------------------
-- |Weak type to distinguish between FilePath and FileName.
type FileName = String
-- |Represents a file. The `anchor` field is the path
-- to that file without the filename.
data AnchoredFile a b =
(:/) { anchor :: FilePath, file :: File a b }
2015-12-17 03:42:22 +00:00
deriving (Eq, Ord, Show)
-- |The String in the name field is always a file name, never a full path.
-- The free type variable is used in the File/Dir constructor and can hold
-- Handles, Strings representing a file's contents or anything else you can
-- think of. We catch any IO errors in the Failed constructor. an Exception
-- can be converted to a String with 'show'.
data File a b =
2015-12-17 03:42:22 +00:00
Failed {
name :: FileName
, err :: IOException
}
| Dir {
name :: FileName
, dir :: a
2015-12-17 03:42:22 +00:00
}
| RegFile {
2015-12-17 03:42:22 +00:00
name :: FileName
, regFile :: b
} deriving (Show, Eq)
-- |All possible file information we could ever need.
data FileInfo = FileInfo {
deviceID :: DeviceID
, fileID :: FileID
, fileMode :: FileMode
, linkCount :: LinkCount
, fileOwner :: UserID
, fileGroup :: GroupID
, specialDeviceID :: DeviceID
, fileSize :: FileOffset
, accessTime :: EpochTime
, modificationTime :: EpochTime
, statusChangeTime :: EpochTime
, accessTimeHiRes :: POSIXTime
, modificationTimeHiRes :: POSIXTime
, statusChangeTimeHiRes :: POSIXTime
, isBlockDevice :: Bool
, isCharacterDevice :: Bool
, isNamedPipe :: Bool
, isRegularFile :: Bool
, isDirectory :: Bool
, isSymbolicLink :: Bool
, isSocket :: Bool
, permissions :: Permissions
} deriving (Show, Eq, Ord)
2015-12-17 03:42:22 +00:00
----------------------------
--[ INSTANCES ]--
----------------------------
instance BF.Bifunctor File where
2015-12-17 03:42:22 +00:00
bimap = BT.bimapDefault
instance BFL.Bifoldable File where
2015-12-17 03:42:22 +00:00
bifoldMap = BT.bifoldMapDefault
instance BT.Bitraversable File where
bitraverse f1 f2 (Dir n b) =
Dir n <$> f1 b
bitraverse _ f2 (RegFile n a) =
RegFile n <$> f2 a
2015-12-17 03:42:22 +00:00
bitraverse _ _ (Failed n e) =
pure (Failed n e)
-- | First compare constructors: Failed < Dir < File...
-- Then compare `name`...
-- Then compare free variable parameter of `File` constructors
instance (Ord a, Ord b, Eq a, Eq b) => Ord (File a b) where
compare (RegFile n a) (RegFile n' a') =
2015-12-17 03:42:22 +00:00
case compare n n' of
EQ -> compare a a'
el -> el
compare (Dir n b) (Dir n' b') =
2015-12-17 03:42:22 +00:00
case compare n n' of
EQ -> compare b b'
2015-12-17 03:42:22 +00:00
el -> el
-- after comparing above we can hand off to shape ord function:
compare d d' = comparingShape d d'
----------------------------
--[ HIGH LEVEL FUNCTIONS ]--
----------------------------
-- |Read a file into an `AnchoredFile`, filling the free variables via
-- `getFileInfo`. This also works on directories, but doesn't look at
-- their contents.
readFileWith :: (FilePath -> IO a)
-> (FilePath -> IO b)
-> FilePath
-> IO (AnchoredFile a b)
readFileWith fd ff fp = do
let fn = topDir fp
bd = baseDir fp
file <- handleDT (topDir fp) $ do
isFile <- doesFileExist fp
if isFile
then RegFile fn <$> ff fp
else Dir fn <$> fd fp
return (bd :/ file)
readFile :: FilePath -> IO (AnchoredFile FileInfo FileInfo)
readFile fp = readFileWith getFileInfo getFileInfo =<< canonicalizePath' fp
-- |Build a list of AnchoredFile, given the path to a directory, filling
-- the free variables via `getFileInfo`. This includes the "." and ".."
-- directories.
readDirectory :: FilePath -> IO [AnchoredFile FileInfo FileInfo]
readDirectory fp = readDirectoryWith getAllDirsFiles getFileInfo getFileInfo
=<< canonicalizePath' fp
-- |Build a list of AnchoredFile, given the path to a directory, filling
-- the free variables via `getFileInfo`. This excludes the "." and ".."
-- directories.
readDirectory' :: FilePath -> IO [AnchoredFile FileInfo FileInfo]
readDirectory' fp = readDirectoryWith getDirsFiles getFileInfo getFileInfo
=<< canonicalizePath' fp
2015-12-17 03:42:22 +00:00
-- | same as readDirectory but allows us to, for example, use
-- ByteString.readFile to return a tree of ByteStrings.
readDirectoryWith :: (FilePath -> IO [FilePath])
-> (FilePath -> IO a)
2015-12-17 03:42:22 +00:00
-> (FilePath -> IO b)
-> FilePath
-> IO [AnchoredFile a b]
readDirectoryWith getfiles fd ff p = buildWith' (buildAtOnce' getfiles) fd ff
=<< canonicalizePath' p
2015-12-17 03:42:22 +00:00
-----------------------------
--[ LOWER LEVEL FUNCTIONS ]--
-----------------------------
-- -- -- helpers: -- -- --
type UserIO a = FilePath -> IO a
type Builder a b = UserIO a -> UserIO b -> FilePath -> IO [File a b]
2015-12-17 03:42:22 +00:00
-- remove non-existent file errors, which are artifacts of the "non-atomic"
-- nature of traversing a system firectory tree:
buildWith' :: Builder a b
-> UserIO a
-> UserIO b
-> FilePath
-> IO [AnchoredFile a b]
2015-12-17 03:42:22 +00:00
buildWith' bf' fd ff p =
do
cfp <- canonicalizePath' p
tree <- bf' fd ff cfp
return $ fmap (cfp :/) (removeNonexistent tree)
2015-12-17 03:42:22 +00:00
-- IO function passed to our builder and finally executed here:
buildAtOnce' :: (FilePath -> IO [FilePath]) -> Builder a b
buildAtOnce' getfiles fd ff fp = do
contents <- getfiles fp
for contents $ \n -> handleDT n $ do
let subf = fp </> n
do isFile <- doesFileExist subf
if isFile
then RegFile n <$> ff subf
else Dir n <$> fd subf
2015-12-17 03:42:22 +00:00
-----------------
--[ UTILITIES ]--
-----------------
---- HANDLING FAILURES ----
-- | True if any Failed constructors in the tree
anyFailed :: [File a b] -> Bool
2015-12-17 03:42:22 +00:00
anyFailed = not . successful
-- | True if there are no Failed constructors in the tree
successful :: [File a b] -> Bool
2015-12-17 03:42:22 +00:00
successful = null . failures
-- | returns true if argument is a `Failed` constructor:
failed :: File a b -> Bool
2015-12-17 03:42:22 +00:00
failed (Failed _ _) = True
failed _ = False
-- | returns a list of 'Failed' constructors only:
failures :: [File a b] -> [File a b]
failures = filter failed
2015-12-17 03:42:22 +00:00
---- ORDERING AND EQUALITY ----
-- | Tests equality of two trees, ignoring their free variable portion. Can be
-- used to check if any files have been added or deleted, for instance.
equalShape :: File a b -> File c d -> Bool
2015-12-17 03:42:22 +00:00
equalShape d d' = comparingShape d d' == EQ
-- TODO: we should use equalFilePath here, but how to sort properly?
-- with System.Directory.canonicalizePath, before compare?
2015-12-17 03:42:22 +00:00
-- | a compare function that ignores the free "file" type variable:
comparingShape :: File a b -> File c d -> Ordering
comparingShape (Dir n _) (Dir n' _) = compare n n'
-- else simply compare the flat constructors, non-recursively:
2015-12-17 03:42:22 +00:00
comparingShape t t' = comparingConstr t t'
-- HELPER: a non-recursive comparison
comparingConstr :: File a b -> File a1 b1 -> Ordering
comparingConstr (Failed _ _) (Dir _ _) = LT
comparingConstr (Failed _ _) (RegFile _ _) = LT
comparingConstr (RegFile _ _) (Failed _ _) = GT
comparingConstr (RegFile _ _) (Dir _ _) = GT
comparingConstr (Dir _ _) (Failed _ _) = GT
comparingConstr (Dir _ _) (RegFile _ _) = LT
2015-12-17 03:42:22 +00:00
-- else compare on the names of constructors that are the same, without
-- looking at the contents of Dir constructors:
comparingConstr t t' = compare (name t) (name t')
---- OTHER ----
---------------
--[ HELPERS ]--
---------------
---- CONSTRUCTOR IDENTIFIERS ----
isFileC :: File a b -> Bool
isFileC (RegFile _ _) = True
isFileC _ = False
2015-12-17 03:42:22 +00:00
isDirC :: File a b -> Bool
isDirC (Dir _ _) = True
isDirC _ = False
2015-12-17 03:42:22 +00:00
---- PATH CONVERSIONS ----
-- extracting pathnames and base names:
topDir, baseDir :: FilePath -> FilePath
topDir = last . splitDirectories
baseDir = joinPath . init . splitDirectories
hiddenFile :: FilePath -> Bool
hiddenFile "." = False
hiddenFile ".." = False
hiddenFile str
| "." `isPrefixOf` str = True
| otherwise = False
2015-12-17 03:42:22 +00:00
-- |Like `normalise` from System.FilePath but removes occurences of '..'.
-- Note that this sort of misbehaves if the path contains symlink
-- components.
normalize :: FilePath -> FilePath
normalize fp =
joinPath $ foldl' ff [] (splitDirectories . normalise $ fp)
where
ff ["/"] ".." = ["/"]
ff x ".." = initDef [] x
ff x y = x ++ [y]
-- |Like `canonicalizePath` from System.Directory, but preserves the last
-- component if it's a symlink.
canonicalizePath' :: FilePath -> IO FilePath
canonicalizePath' fp = do
2015-12-21 17:32:53 +00:00
-- TODO: throw fileDoesNotExist error earlier
isSymlink <- PF.isSymbolicLink <$> PF.getSymbolicLinkStatus fp
if isSymlink
then do
cbase <- canonicalizePath (baseDir fp)
return $ cbase </> topDir fp
else canonicalizePath fp
2015-12-17 03:42:22 +00:00
---- IO HELPERS: ----
-- |Go up one directory in the filesystem hierarchy.
goUp :: AnchoredFile FileInfo FileInfo -> IO (AnchoredFile FileInfo FileInfo)
goUp af@("" :/ _) = return af
goUp (bp :/ _) = Data.DirTree.readFile bp
2015-12-17 03:42:22 +00:00
goUp' :: FilePath -> IO (AnchoredFile FileInfo FileInfo)
goUp' fp = do
cfp <- canonicalizePath' fp
Data.DirTree.readFile $ baseDir cfp
getContents :: AnchoredFile FileInfo FileInfo
-> IO [AnchoredFile FileInfo FileInfo]
getContents (bp :/ Dir n _) = readDirectory (bp </> n)
getContents _ = return []
-- |Get all files of a given directory and return them as a List.
-- This includes "." and "..".
getAllDirsFiles :: FilePath -> IO [FilePath]
getAllDirsFiles fp = do
dirstream <- PFD.openDirStream fp
let mdirs :: [FilePath] -> IO [FilePath]
mdirs dirs = do
dir <- PFD.readDirStream dirstream
if dir == ""
then return dirs
else mdirs (dir : dirs)
dirs <- mdirs []
PFD.closeDirStream dirstream
return dirs
-- |Get all files of a given directory and return them as a List.
-- This excludes "." and "..".
getDirsFiles :: FilePath -> IO [FilePath]
getDirsFiles fp = do
dirstream <- PFD.openDirStream fp
let mdirs :: [FilePath] -> IO [FilePath]
mdirs dirs = do
dir <- PFD.readDirStream dirstream
if dir == ""
then return dirs
else mdirs (insert dir dirs)
dirs <- mdirs []
PFD.closeDirStream dirstream
return dirs
where
insert dir dirs = case dir of
"." -> dirs
".." -> dirs
_ -> dir : dirs
2015-12-17 03:42:22 +00:00
-- |Gets all file information.
getFileInfo :: FilePath -> IO FileInfo
getFileInfo fp = do
fs <- PF.getSymbolicLinkStatus fp
perms <- getPermissions fp
return $ FileInfo
(PF.deviceID fs)
(PF.fileID fs)
(PF.fileMode fs)
(PF.linkCount fs)
(PF.fileOwner fs)
(PF.fileGroup fs)
(PF.specialDeviceID fs)
(PF.fileSize fs)
(PF.accessTime fs)
(PF.modificationTime fs)
(PF.statusChangeTime fs)
(PF.accessTimeHiRes fs)
(PF.modificationTimeHiRes fs)
(PF.statusChangeTimeHiRes fs)
(PF.isBlockDevice fs)
(PF.isCharacterDevice fs)
(PF.isNamedPipe fs)
(PF.isRegularFile fs)
(PF.isDirectory fs)
(PF.isSymbolicLink fs)
(PF.isSocket fs)
perms
-- |Gets the free variable. Returns Nothing if the constructor is of `Failed`.
getFreeVar :: File a a -> Maybe a
getFreeVar (RegFile _ f) = Just f
getFreeVar (Dir _ d) = Just d
getFreeVar _ = Nothing
2015-12-17 03:42:22 +00:00
---- FAILURE HELPERS: ----
-- handles an IO exception by returning a Failed constructor filled with that
-- exception:
handleDT :: FileName -> IO (File a b) -> IO (File a b)
2015-12-17 03:42:22 +00:00
handleDT n = handle (return . Failed n)
-- DoesNotExist errors not present at the topmost level could happen if a
-- named file or directory is deleted after being listed by
-- getDirectoryContents but before we can get it into memory.
-- So we filter those errors out because the user should not see errors
-- raised by the internal implementation of this module:
-- This leaves the error if it exists in the top (user-supplied) level:
removeNonexistent :: [File a b] -> [File a b]
removeNonexistent = filter isOkConstructor
2015-12-17 03:42:22 +00:00
where isOkConstructor c = not (failed c) || isOkError c
isOkError = not . isDoesNotExistErrorType . ioeGetErrorType . err
---- OTHER: ----
fullPath :: AnchoredFile a b -> FilePath
fullPath (bp :/ f) = bp </> name f
2015-12-17 03:42:22 +00:00
fromFreeVar :: (Default d) => (a -> d) -> File a a -> d
fromFreeVar f df = maybeD f $ getFreeVar df
2015-12-17 03:42:22 +00:00
maybeD :: (Default b) => (a -> b) -> Maybe a -> b
maybeD = maybe def
-- |Pack the modification time
packModTime :: File FileInfo FileInfo
2015-12-17 03:42:22 +00:00
-> String
packModTime = fromFreeVar
$ show . posixSecondsToUTCTime . realToFrac . modificationTime
2015-12-17 03:42:22 +00:00
packPermissions :: File FileInfo FileInfo
2015-12-17 03:42:22 +00:00
-> String
packPermissions dt = fromFreeVar (pStr . permissions) dt
where
pStr perm = str perm readable "r"
++ str perm writable "w"
++ str perm (if isDirC dt then searchable else executable)
"x"
str perm f ch
| f perm = ch
| otherwise = "-"