darcs-2.16.4: src/Darcs/Util/Tree/Monad.hs
-- Copyright (C) 2009-2011 Petr Rockai
--
-- BSD3
-- | A monadic interface to Tree mutation. The main idea is to
-- simulate IO-ish manipulation of real filesystem (that's the state part of
-- the monad), and to keep memory usage down by reasonably often dumping the
-- intermediate data to disk and forgetting it. The monad interface itself is
-- generic, and a number of actual implementations can be used. This module
-- provides just 'virtualTreeIO' that never writes any changes, but may trigger
-- filesystem reads as appropriate.
module Darcs.Util.Tree.Monad
( -- * 'TreeMonad'
TreeMonad
, TreeState(tree)
, runTreeMonad
, virtualTreeMonad
-- * Specializing to 'IO'
, TreeIO
, virtualTreeIO
-- * Read actions
, readFile
, exists
, directoryExists
, fileExists
-- * Write actions
, writeFile
, createDirectory
, unlink
, rename
, copy
-- * Other actions
, findM, findFileM, findTreeM
) where
import Darcs.Prelude hiding ( readFile, writeFile )
import Control.Exception ( throw )
import Darcs.Util.Path
import Darcs.Util.Tree
import Data.List( sortBy )
import Data.Int( Int64 )
import Data.Maybe( isNothing, isJust )
import qualified Data.ByteString.Lazy as BL
import Control.Monad.RWS.Strict
import qualified Data.Map as M
-- | Keep track of the size and age of changes to the tree.
type Changed = M.Map AnchoredPath (Int64, Int64) -- size, age
-- | Internal state of the 'TreeMonad'. Keeps track of the current 'Tree'
-- content and unsync'd changes.
data TreeState m = TreeState
{ tree :: !(Tree m)
, changed :: !Changed
, changesize :: !Int64
, maxage :: !Int64
}
data TreeEnv m = TreeEnv
{ updateHash :: TreeItem m -> m Hash
, update :: AnchoredPath -> TreeItem m -> TreeMonad m (TreeItem m)
}
-- | A monad transformer that adds state of type 'TreeState' and an environment
-- of type 'AnchoredPath' (for the current directory).
type TreeMonad m = RWST (TreeEnv m) () (TreeState m) m
-- | 'TreeMonad' specialized to 'IO'
type TreeIO = TreeMonad IO
initialEnv :: (TreeItem m -> m Hash)
-> (AnchoredPath -> TreeItem m -> TreeMonad m (TreeItem m))
-> TreeEnv m
initialEnv uh u = TreeEnv {updateHash = uh, update = u}
initialState :: Tree m -> TreeState m
initialState t =
TreeState {tree = t, changed = M.empty, changesize = 0, maxage = 0}
flush :: Monad m => TreeMonad m ()
flush = do changed' <- map fst . M.toList <$> gets changed
dirs' <- gets tree >>= \t -> return [ path | (path, SubTree _) <- list t ]
modify $ \st -> st { changed = M.empty, changesize = 0 }
forM_ (changed' ++ dirs' ++ [AnchoredPath []]) flushItem
runTreeMonad' :: Monad m => TreeMonad m a -> TreeEnv m -> TreeState m -> m (a, Tree m)
runTreeMonad' action initEnv initState = do
(out, final, _) <- runRWST action initEnv initState
return (out, tree final)
runTreeMonad :: Monad m
=> TreeMonad m a
-> Tree m
-> (TreeItem m -> m Hash)
-> (AnchoredPath -> TreeItem m -> TreeMonad m (TreeItem m))
-> m (a, Tree m)
runTreeMonad action t uh u = do
let action' = do x <- action
flush
return x
runTreeMonad' action' (initialEnv uh u) (initialState t)
-- | Run a 'TreeMonad' action without storing any changes. This is useful for
-- running monadic tree mutations for obtaining the resulting 'Tree' (as opposed
-- to their effect of writing a modified tree to disk). The actions can do both
-- read and write -- reads are passed through to the actual filesystem, but the
-- writes are held in memory in the form of a modified 'Tree'.
virtualTreeMonad :: Monad m => TreeMonad m a -> Tree m -> m (a, Tree m)
virtualTreeMonad action t =
runTreeMonad action t (\_ -> return NoHash) (\_ x -> return x)
-- | 'virtualTreeMonad' specialized to 'IO'
virtualTreeIO :: TreeIO a -> Tree IO -> IO (a, Tree IO)
virtualTreeIO = virtualTreeMonad
-- | Modifies an item in the current Tree. This action keeps an account of the
-- modified data, in changed and changesize, for subsequent flush
-- operations. Any modifications (as in "modifyTree") are allowed.
modifyItem :: Monad m
=> AnchoredPath -> Maybe (TreeItem m) -> TreeMonad m ()
modifyItem path item = do
age <- gets maxage
changed' <- gets changed
let getsize (Just (File b)) = lift (BL.length <$> readBlob b)
getsize _ = return 0
size <- getsize item
let change = case M.lookup path changed' of
Nothing -> size
Just (oldsize, _) -> size - oldsize
modify $ \st -> st { tree = modifyTree (tree st) path item
, changed = M.insert path (size, age) (changed st)
, maxage = age + 1
, changesize = changesize st + change }
renameChanged :: Monad m
=> AnchoredPath -> AnchoredPath -> TreeMonad m ()
renameChanged from to = modify $ \st -> st {changed = rename' $ changed st}
where
rename' = M.fromList . map renameone . M.toList
renameone (x, d)
| from `isPrefix` x = (to `catPaths` relative from x, d)
| otherwise = (x, d)
relative (AnchoredPath from') (AnchoredPath x) =
AnchoredPath $ drop (length from') x
-- | Replace an item with a new version without modifying the content of the
-- tree. This does not do any change tracking. Ought to be only used from a
-- 'sync' implementation for a particular storage format. The presumed use-case
-- is that an existing in-memory Blob is replaced with a one referring to an
-- on-disk file.
replaceItem :: Monad m
=> AnchoredPath -> Maybe (TreeItem m) -> TreeMonad m ()
replaceItem path item = do
modify $ \st -> st { tree = modifyTree (tree st) path item }
flushItem :: forall m. Monad m => AnchoredPath -> TreeMonad m ()
flushItem path =
do current <- gets tree
case find current path of
Nothing -> return () -- vanished, do nothing
Just x -> do y <- fixHash x
new <- asks update >>= ($ y) . ($ path)
replaceItem path (Just new)
where fixHash :: TreeItem m -> TreeMonad m (TreeItem m)
fixHash f@(File (Blob con NoHash)) = do
hash <- asks updateHash >>= \x -> lift $ x f
return $ File $ Blob con hash
fixHash (SubTree s) | treeHash s == NoHash =
asks updateHash >>= \f -> SubTree <$> lift (addMissingHashes f s)
fixHash x = return x
-- | If buffers are becoming large, sync, otherwise do nothing.
flushSome :: Monad m => TreeMonad m ()
flushSome = do x <- gets changesize
when (x > megs 100) $ do
remaining <- go =<< sortBy age . M.toList <$> gets changed
modify $ \s -> s { changed = M.fromList remaining }
where go [] = return []
go ((path, (size, _)):chs) = do
x <- subtract size <$> gets changesize
flushItem path
modify $ \s -> s { changesize = x }
if x > megs 50 then go chs
else return chs
megs = (* (1024 * 1024))
age (_, (_, a)) (_, (_, b)) = compare a b
-- read only actions
expandTo :: Monad m => AnchoredPath -> TreeMonad m ()
expandTo p =
do t <- gets tree
t' <- lift $ expandPath t p
modify $ \st -> st { tree = t' }
-- | Check for existence of a file.
fileExists :: Monad m => AnchoredPath -> TreeMonad m Bool
fileExists p =
do expandTo p
(isJust . (`findFile` p)) <$> gets tree
-- | Check for existence of a directory.
directoryExists :: Monad m => AnchoredPath -> TreeMonad m Bool
directoryExists p =
do expandTo p
(isJust . (`findTree` p)) <$> gets tree
-- | Check for existence of a node (file or directory, doesn't matter).
exists :: Monad m => AnchoredPath -> TreeMonad m Bool
exists p =
do expandTo p
isJust . (`find` p) <$> gets tree
-- | Grab content of a file in the current Tree at the given path.
readFile :: Monad m => AnchoredPath -> TreeMonad m BL.ByteString
readFile p =
do expandTo p
t <- gets tree
let f = findFile t p
case f of
Nothing -> throw $ userError $ "No such file " ++ displayPath p
Just x -> lift (readBlob x)
-- | Change content of a file at a given path. The change will be
-- eventually flushed to disk, but might be buffered for some time.
writeFile :: Monad m => AnchoredPath -> BL.ByteString -> TreeMonad m ()
writeFile p con =
do expandTo p
modifyItem p (Just blob)
flushSome
where blob = File $ Blob (return con) hash
hash = NoHash -- we would like to say "sha256 con" here, but due
-- to strictness of Hash in Blob, this would often
-- lead to unnecessary computation which would then
-- be discarded anyway; we rely on the sync
-- implementation to fix up any NoHash occurrences
-- | Create a directory.
createDirectory :: Monad m => AnchoredPath -> TreeMonad m ()
createDirectory p =
do expandTo p
modifyItem p $ Just $ SubTree emptyTree
-- | Remove the item at a path.
unlink :: Monad m => AnchoredPath -> TreeMonad m ()
unlink p =
do expandTo p
modifyItem p Nothing
-- | Rename the item at a path.
rename :: Monad m => AnchoredPath -> AnchoredPath -> TreeMonad m ()
rename from to =
do expandTo from
expandTo to
tr <- gets tree
let item = find tr from
found_to = find tr to
unless (isNothing found_to) $
throw $ userError $ "Error renaming: destination " ++ displayPath to ++ " exists."
if isJust item then do
modifyItem from Nothing
modifyItem to item
renameChanged from to
else
throw $ userError $ "Error renaming: source " ++ displayPath from ++ " does not exist."
-- | Copy an item from some path to another path.
copy :: Monad m => AnchoredPath -> AnchoredPath -> TreeMonad m ()
copy from to =
do expandTo from
expandTo to
tr <- gets tree
let item = find tr from
unless (isNothing item) $ modifyItem to item
findM' :: forall m a . Monad m
=> (Tree m -> AnchoredPath -> a) -> Tree m -> AnchoredPath -> m a
findM' what t path = fst <$> virtualTreeMonad (look path) t
where look :: AnchoredPath -> TreeMonad m a
look p = expandTo p >> flip what p <$> gets tree
findM :: Monad m => Tree m -> AnchoredPath -> m (Maybe (TreeItem m))
findM = findM' find
findTreeM :: Monad m => Tree m -> AnchoredPath -> m (Maybe (Tree m))
findTreeM = findM' findTree
findFileM :: Monad m => Tree m -> AnchoredPath -> m (Maybe (Blob m))
findFileM = findM' findFile