corebot-bliki-0.2: src/Yesod/CoreBot/Bliki/Resources/Data.hs
{-# LANGUAGE NamedFieldPuns #-}
module Yesod.CoreBot.Bliki.Resources.Data where
import Yesod.CoreBot.Bliki.Prelude
import Yesod.CoreBot.Bliki.Resources.Base
import Yesod.CoreBot.Bliki.Cache.UpdateHTML
import Yesod.CoreBot.Bliki.Config
import Yesod.CoreBot.Bliki.DB
import Yesod.CoreBot.Bliki.Store
import Control.Monad.Reader hiding ( lift )
import Control.Monad.State.Strict hiding ( lift )
import qualified Data.FileStore as FileStore
import Data.Map ( Map )
import qualified Data.Map as Map
import qualified Data.Text as T
import Data.Time.Clock
import Data.Time.Clock.POSIX
import System.Directory ( createDirectory
, doesDirectoryExist
, removeDirectoryRecursive
)
-- XXX: should be a RWST
type DataM master a = StateT DB ( StateT Store (ReaderT ( Config master ) IO) ) a
process_revisions :: Yesod master => ( Config master, Store ) -> IORef DB -> [ Revision ] -> IO ()
process_revisions ( config, store ) db_ref rs = do
db <- readIORef db_ref
let db_mod = execStateT (apply_revisions rs) db
db' <- runReaderT ( evalStateT db_mod store ) config
writeIORef db_ref db'
apply_revisions :: Yesod master => [ Revision ] -> DataM master ()
apply_revisions [] = do
return ()
apply_revisions [ r ] = do
let new_updates = revision_to_updates r
apply_updates new_updates
modify $ \db -> db { raw_history = r : raw_history db }
return ()
apply_revisions (r : r_prev : rs) = do
apply_revisions ( r_prev : rs )
let new_updates = revision_to_updates r
apply_updates new_updates
modify $ \db -> db { raw_history = r : raw_history db }
return ()
apply_updates :: Yesod master => [ DataUpdate ] -> DataM master ()
apply_updates [] = do
return ()
apply_updates ( u : us ) = do
apply_updates us
modify $ \db -> db { update_log = u : update_log db }
f u
where
f ( Wibble _ ) = do
return ()
f ( Tweet _ _ ) = do
return ()
f ( BlogAdded update_rev_ID blog_str ) = do
b <- add_bloggable $ UpdateBloggable blog_str update_rev_ID
build_blog_HTML ( prev_bloggable b ) update_rev_ID blog_str
f ( EntryAdded update_rev_ID update_entry_path ) = do
revs <- gets latest_revisions
let revs' = Map.insert update_entry_path update_rev_ID revs
modify $ \db -> db { latest_revisions = revs' }
b <- add_bloggable $ WikiBloggable update_entry_path update_rev_ID
lift $ build_node_HTML (prev_bloggable b) update_rev_ID update_entry_path
f ( EntryChanged update_rev_ID update_entry_path ) = do
revs <- gets latest_revisions
let revs' = Map.insert update_entry_path update_rev_ID revs
modify $ \db -> db { latest_revisions = revs' }
bs <- gets bloggables
b <- case bs of
( WikiBloggable blog_entry view_rev prev_bloggable : rest_bs )
| blog_entry == update_entry_path -> do
let b' = WikiBloggable blog_entry update_rev_ID prev_bloggable
modify $ \db -> db { bloggables = b' : rest_bs }
return b'
_ -> do
add_bloggable $ WikiBloggable update_entry_path update_rev_ID
lift $ build_node_HTML ( prev_bloggable b ) update_rev_ID update_entry_path
add_bloggable :: ( Maybe Bloggable -> Bloggable ) -> DataM master Bloggable
add_bloggable fb = do
bs <- gets bloggables
let b = case bs of
[] -> fb Nothing
b_prev : _ -> fb $ Just b_prev
modify $ \db -> db { bloggables = b : bs }
return b
-- XXX: Should be event driven but that'd be harder
update_thread :: Yesod master => ( Config master, Store ) -> IORef DB -> IO ()
update_thread ( config, store ) db_ref = do
-- XXX: Lower bound to FileStore.history is not exclusive
let inc_a_bit = addUTCTime (fromInteger 1)
prev_time_ref <- newIORef =<< return . inc_a_bit =<< head_time db_ref
let update_thread_ = do
prev_time <- readIORef prev_time_ref
putStrLn $ "probing for changes since " ++ show prev_time
rs <- FileStore.history ( filestore store )
[]
( TimeRange (Just prev_time)
Nothing
)
case null rs of
True -> return ()
False -> do
putStrLn "found updates"
process_revisions ( config, store ) db_ref rs
writeIORef prev_time_ref =<< return . inc_a_bit
=<< head_time db_ref
-- delay before probing for updates again
threadDelay 10000000
forever update_thread_
mk_data :: Yesod master => Config master -> IO ( Data_ master )
mk_data config = do
-- clear memoization store
should_clear_memo_store <- doesDirectoryExist $ cache_dir config
when should_clear_memo_store $ removeDirectoryRecursive $ cache_dir config
createDirectory $ cache_dir config
-- build internal DB state
let filestore = FileStore.gitFileStore $ store_dir config
store = Store { filestore = filestore }
empty_db = DB [] [] Map.empty []
initial_history <- FileStore.history filestore [] (TimeRange Nothing Nothing)
let db_0_build = execStateT (apply_revisions initial_history) empty_db
db_ref <- newIORef =<< runReaderT (evalStateT db_0_build store) config
-- XXX: Only because store is not pure value but a reference
the_ID <- forkIO $ update_thread ( config, store ) db_ref
return Data { config = config
, store = store
, update_thread_ID = the_ID
, db_ref = db_ref
}
node_HTML_content :: Yesod master => Data_ master -> DB -> FilePath -> Content
node_HTML_content src_data db node_path =
let Just rev_ID = Map.lookup node_path ( latest_revisions db )
out_path = node_HTML_path (config src_data) rev_ID node_path
in ContentFile out_path Nothing
blog_HTML_content :: Yesod master => Data_ master -> RevisionId -> Content
blog_HTML_content src_data rev_ID =
let out_path = blog_HTML_path (config src_data) rev_ID
in ContentFile out_path Nothing
getBlogR :: Yesod master => RevisionId -> GHandler ( Data_ master ) master [(ContentType, Content)]
getBlogR rev_ID = do
src_data <- getYesodSub
let out_HTML_content = blog_HTML_content src_data rev_ID
return [ ( typeHtml, out_HTML_content )
]
getLatestR :: Yesod master => GHandler ( Data_ master ) master [(ContentType, Content)]
getLatestR = do
src_data <- getYesodSub
db <- liftIO $ readIORef $ db_ref src_data
let latest = head $ bloggables db
case latest of
UpdateBloggable blog_str source_rev _ -> do
let out_HTML = blog_HTML_content src_data source_rev
return [ ( typeHtml, out_HTML )
, ( typePlain, toContent blog_str )
]
WikiBloggable blog_entry _ _ -> do
let markdown_path = node_markdown_path (config src_data) blog_entry
let out_HTML = node_HTML_content src_data db blog_entry
return [ ( typeHtml , out_HTML )
, ( typePlain, ContentFile markdown_path Nothing )
]
getUpdateLogR :: Yesod master => GHandler ( Data_ master ) master RepJson
getUpdateLogR = do
jsonToRepJson $ toJSON ()
getEntryRevR :: Yesod master
=> RevisionId
-> [ Text ]
-> GHandler ( Data_ master ) master [(ContentType, Content)]
getEntryRevR rev_ID entry_path_texts = do
src_data <- getYesodSub
let ( first_path : rest_paths ) = map T.unpack entry_path_texts
node_path = foldl (</>) first_path rest_paths
let p = node_HTML_path (config src_data) rev_ID node_path
let markdown_path = node_markdown_path (config src_data) node_path
return [ ( typeHtml , ContentFile p Nothing )
, ( typePlain, ContentFile markdown_path Nothing )
]
getEntryLatestR :: Yesod master => [ Text ] -> GHandler ( Data_ master ) master [(ContentType, Content)]
getEntryLatestR entry_path_texts = do
let ( first_path : rest_paths ) = map T.unpack entry_path_texts
node_path = foldl (</>) first_path rest_paths
src_data <- getYesodSub
db <- liftIO $ readIORef $ db_ref src_data
let x = Map.lookup node_path ( latest_revisions db )
case x of
Nothing -> do
liftIO $ putStrLn $ "no node at path " ++ show node_path
fail $ "no node at path " ++ show node_path
Just rev_ID -> do
liftIO $ putStrLn $ "latest rev of " ++ show node_path ++ " is " ++ show rev_ID
getEntryRevR rev_ID entry_path_texts
mkYesodSubDispatch "Data_ master" [] [parseRoutes|
/latest LatestR GET
/ UpdateLogR GET
/entry/*Texts EntryLatestR GET
/blog/#RevisionId BlogR GET
/rev/#RevisionId/*Texts EntryRevR GET
|]