2015-12-17 03:42:22 +00:00
|
|
|
{-# OPTIONS_HADDOCK ignore-exports #-}
|
|
|
|
|
2015-12-19 15:13:48 +00:00
|
|
|
-- |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
|
2015-12-21 16:15:31 +00:00
|
|
|
, foldl'
|
2015-12-21 04:41:12 +00:00
|
|
|
, isPrefixOf
|
2015-12-17 03:42:22 +00:00
|
|
|
, sort
|
|
|
|
, sortBy
|
|
|
|
, (\\)
|
|
|
|
)
|
2015-12-19 15:13:48 +00:00
|
|
|
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
|
|
|
(
|
2015-12-19 15:13:48 +00:00
|
|
|
for
|
2015-12-17 03:42:22 +00:00
|
|
|
)
|
|
|
|
import Data.Word
|
|
|
|
(
|
|
|
|
Word64
|
|
|
|
)
|
2015-12-20 23:41:02 +00:00
|
|
|
import Safe
|
|
|
|
(
|
|
|
|
atDef
|
2015-12-21 16:15:31 +00:00
|
|
|
, initDef
|
2015-12-20 23:41:02 +00:00
|
|
|
)
|
2015-12-17 03:42:22 +00:00
|
|
|
import System.Directory
|
|
|
|
(
|
2015-12-21 04:41:12 +00:00
|
|
|
canonicalizePath
|
|
|
|
, doesFileExist
|
2015-12-19 15:13:48 +00:00
|
|
|
, executable
|
2015-12-17 03:42:22 +00:00
|
|
|
, getPermissions
|
2015-12-19 15:13:48 +00:00
|
|
|
, readable
|
2015-12-17 03:42:22 +00:00
|
|
|
, searchable
|
2015-12-19 15:13:48 +00:00
|
|
|
, writable
|
|
|
|
, Permissions
|
2015-12-17 03:42:22 +00:00
|
|
|
)
|
|
|
|
import System.FilePath
|
|
|
|
(
|
|
|
|
combine
|
2015-12-21 16:15:31 +00:00
|
|
|
, normalise
|
2015-12-17 03:42:22 +00:00
|
|
|
, equalFilePath
|
|
|
|
, joinPath
|
|
|
|
, splitDirectories
|
2015-12-20 23:41:02 +00:00
|
|
|
, 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
|
|
|
|
)
|
2015-12-19 15:13:48 +00:00
|
|
|
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
|
2015-12-19 15:13:48 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
-- |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'.
|
2015-12-20 23:41:02 +00:00
|
|
|
data File a b =
|
2015-12-17 03:42:22 +00:00
|
|
|
Failed {
|
|
|
|
name :: FileName
|
|
|
|
, err :: IOException
|
|
|
|
}
|
|
|
|
| Dir {
|
2015-12-19 15:13:48 +00:00
|
|
|
name :: FileName
|
|
|
|
, dir :: a
|
2015-12-17 03:42:22 +00:00
|
|
|
}
|
2015-12-20 23:41:02 +00:00
|
|
|
| RegFile {
|
2015-12-17 03:42:22 +00:00
|
|
|
name :: FileName
|
2015-12-20 23:41:02 +00:00
|
|
|
, regFile :: b
|
2015-12-19 15:13:48 +00:00
|
|
|
} 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
|
|
|
|
|
|
|
|
2015-12-22 18:40:29 +00:00
|
|
|
isSymL :: AnchoredFile FileInfo FileInfo
|
|
|
|
-> (Bool, AnchoredFile FileInfo FileInfo)
|
|
|
|
isSymL f@(_ :/ Dir _ FileInfo { isSymbolicLink = True }) = (True, f)
|
|
|
|
isSymL f@(_ :/ RegFile _ FileInfo { isSymbolicLink = True }) = (True, f)
|
|
|
|
isSymL f = (False, f)
|
|
|
|
|
|
|
|
|
|
|
|
pattern IsSymL b <- (isSymL -> (b, f))
|
|
|
|
|
|
|
|
|
2015-12-17 03:42:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
----------------------------
|
|
|
|
--[ INSTANCES ]--
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
instance BF.Bifunctor File where
|
2015-12-17 03:42:22 +00:00
|
|
|
bimap = BT.bimapDefault
|
|
|
|
|
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
instance BFL.Bifoldable File where
|
2015-12-17 03:42:22 +00:00
|
|
|
bifoldMap = BT.bifoldMapDefault
|
|
|
|
|
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
instance BT.Bitraversable File where
|
2015-12-19 15:13:48 +00:00
|
|
|
bitraverse f1 f2 (Dir n b) =
|
|
|
|
Dir n <$> f1 b
|
2015-12-20 23:41:02 +00:00
|
|
|
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
|
2015-12-20 23:41:02 +00:00
|
|
|
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
|
2015-12-19 15:13:48 +00:00
|
|
|
compare (Dir n b) (Dir n' b') =
|
2015-12-17 03:42:22 +00:00
|
|
|
case compare n n' of
|
2015-12-19 15:13:48 +00:00
|
|
|
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 ]--
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
-- |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
|
2015-12-22 13:15:48 +00:00
|
|
|
let fn = topDir fp
|
|
|
|
bd = baseDir fp
|
|
|
|
file <- handleDT (topDir fp) $ do
|
|
|
|
isFile <- doesFileExist fp
|
2015-12-20 23:41:02 +00:00
|
|
|
if isFile
|
2015-12-22 13:15:48 +00:00
|
|
|
then RegFile fn <$> ff fp
|
|
|
|
else Dir fn <$> fd fp
|
2015-12-20 23:41:02 +00:00
|
|
|
return (bd :/ file)
|
|
|
|
|
|
|
|
|
|
|
|
readFile :: FilePath -> IO (AnchoredFile FileInfo FileInfo)
|
2015-12-21 16:15:31 +00:00
|
|
|
readFile fp = readFileWith getFileInfo getFileInfo =<< canonicalizePath' fp
|
2015-12-20 23:41:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
-- |Build a list of AnchoredFile, given the path to a directory, filling
|
2015-12-22 13:15:48 +00:00
|
|
|
-- the free variables via `getFileInfo`. This includes the "." and ".."
|
|
|
|
-- directories.
|
2015-12-20 23:41:02 +00:00
|
|
|
readDirectory :: FilePath -> IO [AnchoredFile FileInfo FileInfo]
|
2015-12-22 13:15:48 +00:00
|
|
|
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
|
2015-12-21 16:15:31 +00:00
|
|
|
=<< 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.
|
2015-12-22 13:15:48 +00:00
|
|
|
readDirectoryWith :: (FilePath -> IO [FilePath])
|
|
|
|
-> (FilePath -> IO a)
|
2015-12-17 03:42:22 +00:00
|
|
|
-> (FilePath -> IO b)
|
|
|
|
-> FilePath
|
2015-12-20 23:41:02 +00:00
|
|
|
-> IO [AnchoredFile a b]
|
2015-12-22 13:15:48 +00:00
|
|
|
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
|
2015-12-20 23:41:02 +00:00
|
|
|
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
|
2015-12-20 23:41:02 +00:00
|
|
|
-> IO [AnchoredFile a b]
|
2015-12-17 03:42:22 +00:00
|
|
|
buildWith' bf' fd ff p =
|
2015-12-21 04:41:12 +00:00
|
|
|
do
|
2015-12-21 16:15:31 +00:00
|
|
|
cfp <- canonicalizePath' p
|
2015-12-21 04:41:12 +00:00
|
|
|
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:
|
2015-12-22 13:15:48 +00:00
|
|
|
buildAtOnce' :: (FilePath -> IO [FilePath]) -> Builder a b
|
|
|
|
buildAtOnce' getfiles fd ff fp = do
|
|
|
|
contents <- getfiles fp
|
2015-12-19 15:13:48 +00:00
|
|
|
for contents $ \n -> handleDT n $ do
|
2015-12-22 13:15:48 +00:00
|
|
|
let subf = fp </> n
|
2015-12-19 15:13:48 +00:00
|
|
|
do isFile <- doesFileExist subf
|
|
|
|
if isFile
|
2015-12-20 23:41:02 +00:00
|
|
|
then RegFile n <$> ff subf
|
2015-12-19 15:13:48 +00:00
|
|
|
else Dir n <$> fd subf
|
2015-12-17 03:42:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-----------------
|
|
|
|
--[ UTILITIES ]--
|
|
|
|
-----------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
---- HANDLING FAILURES ----
|
|
|
|
|
|
|
|
|
|
|
|
-- | True if any Failed constructors in the tree
|
2015-12-20 23:41:02 +00:00
|
|
|
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
|
2015-12-20 23:41:02 +00:00
|
|
|
successful :: [File a b] -> Bool
|
2015-12-17 03:42:22 +00:00
|
|
|
successful = null . failures
|
|
|
|
|
|
|
|
|
|
|
|
-- | returns true if argument is a `Failed` constructor:
|
2015-12-20 23:41:02 +00:00
|
|
|
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:
|
2015-12-20 23:41:02 +00:00
|
|
|
failures :: [File a b] -> [File a b]
|
2015-12-19 15:13:48 +00:00
|
|
|
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.
|
2015-12-20 23:41:02 +00:00
|
|
|
equalShape :: File a b -> File c d -> Bool
|
2015-12-17 03:42:22 +00:00
|
|
|
equalShape d d' = comparingShape d d' == EQ
|
|
|
|
|
2015-12-19 15:13:48 +00:00
|
|
|
-- 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:
|
2015-12-20 23:41:02 +00:00
|
|
|
comparingShape :: File a b -> File c d -> Ordering
|
2015-12-19 15:13:48 +00:00
|
|
|
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
|
2015-12-20 23:41:02 +00:00
|
|
|
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 ----
|
2015-12-20 23:41:02 +00:00
|
|
|
isFileC :: File a b -> Bool
|
|
|
|
isFileC (RegFile _ _) = True
|
|
|
|
isFileC _ = False
|
2015-12-17 03:42:22 +00:00
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
isDirC :: File a b -> Bool
|
2015-12-19 15:13:48 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2015-12-21 04:41:12 +00:00
|
|
|
hiddenFile :: FilePath -> Bool
|
|
|
|
hiddenFile "." = False
|
|
|
|
hiddenFile ".." = False
|
|
|
|
hiddenFile str
|
|
|
|
| "." `isPrefixOf` str = True
|
|
|
|
| otherwise = False
|
|
|
|
|
2015-12-17 03:42:22 +00:00
|
|
|
|
2015-12-21 16:15:31 +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
|
2015-12-21 16:15:31 +00:00
|
|
|
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: ----
|
|
|
|
|
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
-- |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
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
|
2015-12-21 04:41:12 +00:00
|
|
|
goUp' :: FilePath -> IO (AnchoredFile FileInfo FileInfo)
|
|
|
|
goUp' fp = do
|
2015-12-21 16:15:31 +00:00
|
|
|
cfp <- canonicalizePath' fp
|
2015-12-21 04:41:12 +00:00
|
|
|
Data.DirTree.readFile $ baseDir cfp
|
|
|
|
|
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
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.
|
2015-12-21 04:41:12 +00:00
|
|
|
-- 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 "..".
|
2015-12-20 23:41:02 +00:00
|
|
|
getDirsFiles :: FilePath -> IO [FilePath]
|
2015-12-19 15:13:48 +00:00
|
|
|
getDirsFiles fp = do
|
|
|
|
dirstream <- PFD.openDirStream fp
|
2015-12-20 23:41:02 +00:00
|
|
|
let mdirs :: [FilePath] -> IO [FilePath]
|
|
|
|
mdirs dirs = do
|
2015-12-19 15:13:48 +00:00
|
|
|
dir <- PFD.readDirStream dirstream
|
|
|
|
if dir == ""
|
|
|
|
then return dirs
|
2015-12-21 04:41:12 +00:00
|
|
|
else mdirs (insert dir dirs)
|
2015-12-20 23:41:02 +00:00
|
|
|
dirs <- mdirs []
|
2015-12-19 15:13:48 +00:00
|
|
|
PFD.closeDirStream dirstream
|
|
|
|
return dirs
|
|
|
|
where
|
2015-12-21 04:41:12 +00:00
|
|
|
insert dir dirs = case dir of
|
2015-12-19 15:13:48 +00:00
|
|
|
"." -> dirs
|
|
|
|
".." -> dirs
|
2015-12-20 23:41:02 +00:00
|
|
|
_ -> dir : dirs
|
2015-12-17 03:42:22 +00:00
|
|
|
|
2015-12-19 15:13:48 +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`.
|
2015-12-20 23:41:02 +00:00
|
|
|
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:
|
2015-12-20 23:41:02 +00:00
|
|
|
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:
|
2015-12-20 23:41:02 +00:00
|
|
|
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: ----
|
|
|
|
|
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
fullPath :: AnchoredFile a b -> FilePath
|
|
|
|
fullPath (bp :/ f) = bp </> name f
|
2015-12-17 03:42:22 +00:00
|
|
|
|
|
|
|
|
2015-12-20 23:41:02 +00:00
|
|
|
fromFreeVar :: (Default d) => (a -> d) -> File a a -> d
|
2015-12-19 15:13:48 +00:00
|
|
|
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
|
2015-12-20 23:41:02 +00:00
|
|
|
packModTime :: File FileInfo FileInfo
|
2015-12-17 03:42:22 +00:00
|
|
|
-> String
|
|
|
|
packModTime = fromFreeVar
|
2015-12-19 15:13:48 +00:00
|
|
|
$ show . posixSecondsToUTCTime . realToFrac . modificationTime
|
2015-12-17 03:42:22 +00:00
|
|
|
|
2015-12-20 23:41:02 +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 = "-"
|