{-# LANGUAGE RankNTypes #-} {-# LANGUAGE TypeFamilies #-} -- | Helper to read a directory of files, and observe it for changes. -- -- Use @new@ in conjunction with @observe@ in your @runEma@ function call. module Ema.Helper.FileSystem ( filesMatching, onChange, FileAction (..), ) where import Control.Concurrent (threadDelay) import Control.Exception (finally) import Control.Monad.Logger import System.Directory (canonicalizePath) import System.FSNotify ( ActionPredicate, Event (..), StopListening, WatchManager, watchTree, withManager, ) import System.FilePath (makeRelative) import System.FilePattern (FilePattern) import System.FilePattern.Directory (getDirectoryFiles) import UnliftIO (MonadUnliftIO, withRunInIO) type FolderPath = FilePath log :: MonadLogger m => LogLevel -> Text -> m () log = logWithoutLoc "Helper.FileSystem" filesMatching :: (MonadIO m, MonadLogger m) => FolderPath -> [FilePattern] -> m [FilePath] filesMatching parent' pats = do parent <- liftIO $ canonicalizePath parent' log LevelInfo $ toText $ "Traversing " <> parent <> " for files matching " <> show pats liftIO $ getDirectoryFiles parent pats data FileAction = Update | Delete deriving (Eq, Show) onChange :: forall m. (MonadIO m, MonadLogger m, MonadUnliftIO m) => FolderPath -> (FilePath -> FileAction -> m ()) -> m () onChange parent' f = do -- NOTE: It is important to use canonical path, because this will allow us to -- transform fsnotify event's (absolute) path into one that is relative to -- @parent'@ (as passed by user), which is what @f@ will expect. parent <- liftIO $ canonicalizePath parent' withManagerM $ \mgr -> do log LevelInfo $ toText $ "Monitoring " <> parent <> " for changes" stop <- watchTreeM mgr parent (const True) $ \event -> do log LevelDebug $ show event let rel = makeRelative parent case event of Added (rel -> fp) _ _ -> f fp Update Modified (rel -> fp) _ _ -> f fp Update Removed (rel -> fp) _ _ -> f fp Delete Unknown (rel -> fp) _ _ -> f fp Delete liftIO $ threadDelay maxBound `finally` stop withManagerM :: (MonadIO m, MonadUnliftIO m) => (WatchManager -> m a) -> m a withManagerM f = do withRunInIO $ \run -> withManager $ \mgr -> run (f mgr) watchTreeM :: forall m. (MonadIO m, MonadUnliftIO m) => WatchManager -> FilePath -> ActionPredicate -> (Event -> m ()) -> m StopListening watchTreeM wm fp pr f = withRunInIO $ \run -> watchTree wm fp pr $ \evt -> run (f evt)