module System.Delta.Poll ( createPollWatcher ) where import Control.Applicative ((<$>)) import Control.Concurrent import Control.Monad (foldM) import qualified Data.Map as M import Data.Maybe (catMaybes) import FRP.Sodium import System.Delta.Base import System.Delta.Class import System.Directory import System.FilePath import Data.List (isPrefixOf) -- | Watch files in this directory recursively for changes every -- n seconds. createPollWatcher :: Int -- ^ seconds interval -> FilePath -- ^ path to watch -> IO FileWatcher createPollWatcher secs path = do (changedEvent, pushChanged) <- sync $ newEvent (deletedEvent, pushDeleted) <- sync $ newEvent (newFileEvent, pushNewFile) <- sync $ newEvent canonPath <- canonicalizePath path watcherId <- startWatchThread canonPath pushNewFile pushDeleted pushChanged secs return $ FileWatcher newFileEvent deletedEvent changedEvent (killThread watcherId) -- | Recursively traverse a folder, follow symbolic links but don't -- visit a file twice. recursiveDescent path = M.filterWithKey (\_ -> not . fileInfoIsDir) <$> -- files only recursiveDescent' M.empty path -- | Recursively traverse a folder, follows symbolic links, -- doesn't loop however. recursiveDescent' :: M.Map FilePath FileInfo -> FilePath -> IO (M.Map FilePath FileInfo) recursiveDescent' visited path | M.member path visited = return visited recursiveDescent' visited path = do isDir <- doesDirectoryExist path inf <- mkFileInfo path let visitedWithCurrent = M.insert path inf visited if not isDir then return $ visitedWithCurrent else do contentsUnfiltered <- getDirectoryContents path let contentsFiltered = filter (\x -> x /= "." && x /= "..") contentsUnfiltered contentsAbs = (combine path) <$> contentsFiltered foldM recursiveDescent' visitedWithCurrent contentsAbs -- | List all files that have a larger modification time in the second -- map than in the first diffChangedFiles :: M.Map FilePath FileInfo -> M.Map FilePath FileInfo -> [FileInfo] diffChangedFiles before after = catMaybes . M.elems $ M.intersectionWith f before after where f beforeInfo afterInfo = if fileInfoTimestamp beforeInfo < fileInfoTimestamp afterInfo then Just afterInfo else Nothing -- | List all files that occur in the second map but not the first diffNewFiles :: M.Map FilePath FileInfo -> M.Map FilePath FileInfo -> [FileInfo] diffNewFiles before after = M.elems $ M.difference after before -- | List all files that occur in the first map but not the second diffDeletedFiles :: M.Map FilePath FileInfo -> M.Map FilePath FileInfo -> [FileInfo] diffDeletedFiles before after = M.elems $ M.difference before after -- | Fork a thread that continuously polls the given paht and compares -- the results of two polls. startWatchThread :: FilePath -> (FilePath -> Reactive ()) -- ^ Push new files / dirs -> (FilePath -> Reactive ()) -- ^ Push deleted files / dirs -> (FilePath -> Reactive ()) -- ^ Push changed files / dirs -> Int -- ^ Seconds between polls -> IO ThreadId startWatchThread path pushNew pushDeleted pushChanged secs = do curr <- recursiveDescent path forkIO $ go curr where go last = do threadDelay $ secs * 1000 * 1000 curr <- recursiveDescent path sync $ mapM_ (pushChanged) (fileInfoPath <$> diffChangedFiles last curr) sync $ mapM_ (pushNew ) (fileInfoPath <$> diffNewFiles last curr ) sync $ mapM_ (pushDeleted) (fileInfoPath <$> diffDeletedFiles last curr) go curr